Index Ai Academy
Phase 1 · Building with LLMs · Quality & production

Shipping an LLM app

Deploy, secure keys, rate-limit and handle failures.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

A script on your laptop becomes a product the moment other people can use it. And every one of them can now spend your money. Shipping an AI app is mostly ordinary web engineering: keep secrets on the server, check inputs, limit abuse and handle failure. The difference is that the thing you depend on is slow, charged per use and sometimes overloaded. Get these right, and a small team can run an AI feature safely. Skip them, and one leaked key or one runaway script can run up a four-figure bill overnight.

What you'll be able to do

  • Put the model call behind your own backend so the API key never reaches a browser
  • Protect the endpoint with input limits, per-user rate limits and spending caps
  • Fail gracefully: friendly errors, no leaked internals, a fallback when the model is busy
  • Walk through a launch checklist for a small LLM feature

What it covers

  1. The shape of an AI app
  2. Protect the endpoint
  3. A rate limit in a few lines
  4. Fail gracefully
  5. Two checks before you spend anything

The shape of an AI app

Think of a bank teller. Customers never get the vault key. They ask at the counter, and the teller, who holds the key, checks the request and fetches what's allowed.

Your backend is the teller. It's the part of your app that runs on your own server:

browser or app  ──►  your backend  ──►  model API
 (no secrets)        (holds the key,      (Anthropic)
                      checks, limits,
                      logs)

The API key lives only on your server. It's read from an environment variable (a setting the server is given when it starts) or a secrets manager. Anything you ship to a browser or a phone can be read by anyone who downloads it. That includes JavaScript and app files. So a key in front-end code is a public key. This academy works exactly this way: lesson code calls a web address, and the academy's server adds the key.

Here's a minimal backend in Python, using the FastAPI web framework and the Anthropic SDK:

import anthropic
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

client = anthropic.Anthropic()      # reads ANTHROPIC_API_KEY from the environment
app = FastAPI()
SYSTEM = "You answer questions about our outdoor clothing shop. ..."

class Question(BaseModel):
    text: str

@app.post("/api/answer")
def answer(question: Question):
    if not question.text.strip() or len(question.text) > 2000:
        raise HTTPException(400, "Questions must be 1 to 2,000 characters.")
    message = client.messages.create(
        model="claude-opus-5-5", max_tokens=500, system=SYSTEM,
        messages=[{"role": "user", "content": question.text}],
    )
    return {"answer": "".join(b.text for b in message.content if b.type == "text")}

What it does:

  • @app.post("/api/answer") makes the function answer requests sent to that address.
  • Question describes the expected input: one piece of text.
  • The if refuses empty questions and anything over 2,000 characters, with a 400 error.
  • Then it calls the model and returns just the text of the answer.

The front end sends the question to /api/answer. It never talks to Anthropic directly. The SDK also retries short-lived errors for you.

Quick check
Why does the page call your server rather than calling Anthropic directly?
  1. Browsers can't send JSON
  2. It makes the replies better
  3. So the API key stays on the server, out of anyone's browser
  4. Anthropic only accepts servers in Europe
Show the answer

So the API key stays on the server, out of anyone's browser

Anything sent to a browser can be read. The key stays on your server, and the page only ever talks to your own endpoint.

Protect the endpoint

Your endpoint spends money every time it's called. Treat it like a payment form:

  1. Know who's calling. Require sign-in, or at least an access code for internal tools. An open endpoint will be found and used.
  2. Limit input size. Cap characters, files and conversation length. Long inputs cost money and give attackers room for injection.
  3. Rate-limit each user. A common method is the token bucket. Each user has a bucket of, say, 5 requests. It refills at one request every 10 seconds. Short bursts are fine, but steady hammering gets a 429 Too Many Requests.
  4. Cap spending. Set monthly spending limits in your provider's console, with alerts well below them. A daily budget per user catches the one account that's running a script on your app.
  5. Set max_tokens and timeouts on every call, so no single request can run away.
Worked example

A rate limit in a few lines

Your endpoint spends money on each call, so limit how often one person can call it. This keeps a count per user for each minute.

from collections import defaultdict

LIMIT = 3
counts = defaultdict(int)

def allowed(user, minute):
    key = (user, minute)
    counts[key] += 1
    return counts[key] <= LIMIT

for n in range(5):
    print("ann", "minute 1:", allowed("ann", 1))
print("ben", "minute 1:", allowed("ben", 1))
print("ann", "minute 2:", allowed("ann", 2))
It runs right here in your browser, with nothing to install.

Ann's fourth and fifth calls get refused, Ben isn't affected, and Ann's count starts fresh next minute. In production you'd store the counts somewhere shared, like Redis, since one server's memory can't see the others. You'd answer refused calls with a 429.

Quick check
What does this print?
import os
os.environ["API_KEY"] = "secret"
print("secret" in "fetch('/api/chat')")
  1. secret
  2. None
  3. False
  4. True
Show the answer

False

The in checks only the text fetch('/api/chat'), which doesn't contain the word secret, so it prints False. The browser only calls your server. The key stays on the server, so it never appears in page code.

Try it now

Three quick requests empty the bucket, the fourth is refused, and two seconds later one token has refilled. It prints ['ok', 'ok', 'ok', '429', 'ok'].

Quick check
In the token bucket above, why is the fourth request refused but the fifth allowed?
  1. The fifth request came from a different user
  2. The first three used up the bucket; two seconds later it had refilled enough for one more
  3. The fourth request was too long
  4. Every fourth request is always refused
Show the answer

The first three used up the bucket; two seconds later it had refilled enough for one more

The bucket holds 3 and refills half a request per second. After three quick requests it's nearly empty. Two seconds later, one more request's worth has refilled.

Fail gracefully

The model API will sometimes be slow, rate-limited or overloaded. Plan for it:

  • Friendly errors, no internals. Show "The assistant is busy, please try again", and log the details for yourself. Never show users the raw error messages from your code or the API. They leak information and confuse people.
  • Fallbacks. If the main model is still overloaded after retries, try a smaller model, a saved answer to a common question, or a link to the help center.
  • Stream long answers, so users see progress, and let them cancel.
  • Timeouts that match your hosting. Some hosts cut requests off after 10 to 60 seconds, which is shorter than a long answer takes. Streaming, or running the work in the background, avoids that.

Deploying it

Hosting services such as Railway, Render, Fly.io and Vercel run a small backend straight from a Git repository. Push to the main branch, and they build and deploy it. You set ANTHROPIC_API_KEY in the host's environment settings, never in the repository. Add a health check, a simple address the host can call to check the app is running. And keep a staging copy of the app, where you try changes before they reach real users.

Launch checklist

AreaCheck
SecretsKey only in server environment; .env files in .gitignore; key replaced if ever exposed
AccessSign-in or access code; rate limits per user; input size limits
Costmax_tokens on every call; caching where prompts repeat; spending limits and alerts
QualityEval passes on the release candidate; grounded prompts; tested against injection examples
FailureRetries with backoff; friendly errors; fallback path; timeouts
OperationsLogging with personal data removed; dashboards for cost, latency, errors; a way to roll back
PeopleUsers know they're talking to AI; a path to a person for anything important

For marketers: before launching a customer-facing AI feature, go through the checklist with your developer and your legal or compliance contact. "What happens when it's wrong?" deserves an answer before launch, not after.

Worked example

Two checks before you spend anything

Before calling the model, the server checks the input and the user's budget for the day. Each user may spend $1.00 a day. Alex has spent 10 cents so far, and Sam has spent 99 cents. A request costs about 2 cents.

MAX_CHARS = 2000
DAILY_BUDGET = 1.00
spent_today = {"sam": 0.99, "alex": 0.10}

def allowed(user, text, cost=0.02):
    if not text.strip() or len(text) > MAX_CHARS:
        return "400: too long or empty"
    if spent_today.get(user, 0) + cost > DAILY_BUDGET:
        return "429: daily budget used"
    return "ok"

print(allowed("alex", "Do gift cards expire?"))
print(allowed("sam", "Do gift cards expire?"))
print(allowed("alex", "x" * 3000))
In the lesson you can change this and run it.

It prints ok, then 429: daily budget used, then 400: too long or empty. Sam's request would bring Sam's total to $1.01, over the $1.00 budget. Alex's last message is 3,000 characters, over the 2,000 limit. Neither call reaches the model, so neither costs a cent.

Quick check
A teammate wants to call the Anthropic API directly from the website's JavaScript "to keep things simple". What's the problem?
  1. The API only works from Python
  2. Anyone can read the API key from the page and spend money on your account
  3. Browsers can't send JSON
  4. It would make replies slower
Show the answer

Anyone can read the API key from the page and spend money on your account

Front-end code is public. The key must stay on a server you control.

Quick check
The model API is overloaded and your retries have run out. What should the user see?
  1. The full error message and code from the API
  2. A friendly message like "The assistant is busy, please try again", with a fallback such as a help-center link
  3. Nothing: keep retrying forever
  4. A blank page
Show the answer

A friendly message like "The assistant is busy, please try again", with a fallback such as a help-center link

Keep the details in your logs. Users get a clear, kind message and somewhere else to go.

Practice what you just read

  • 3 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

Where should your Anthropic API key live in a web app?

  1. In an environment variable on your backend server
  2. In the website's JavaScript, minified
  3. In the mobile app's settings file
  4. In the Git repository, in a private folder

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson