AI Development · Level 1 · Beginner

Deploy a Small AI Web App

Put a small AI app online safely: a static page plus one serverless function, the API key in an environment variable, rate limits, HTTPS, logs and cost control.

By 9 min read
Put a small AI app online safely: a static page plus one serverless function, the API key in an environment variable, rate limits, HTTPS, logs and cost control.

Your script from part 1 works on your laptop. To let other people use it, it has to run on the internet — without exposing your API key, without inviting abuse and without a surprise bill. Putting software on a server so that people can reach it is called deployment.

This is part 5, the last part of Level 1 in our AI Development learning path. You will set up the simplest sound architecture for a small AI web app: a static page and one serverless function. You will learn where the API key lives, how to limit requests and costs, how to use your own domain with HTTPS and what to watch after launch. The guide builds on your first LLM API call and on the parts of an AI app from Level 0.

The architecture: a static page and one function

A small AI web app needs only two parts of its own:

  • The static frontend: HTML, CSS and JavaScript files that the server delivers unchanged — your page with the chat window. Hosting platforms usually serve such files from a CDN (content delivery network): servers in many places that keep copies of your files close to your visitors.
  • A serverless function: a small piece of backend code that the platform runs only when a request arrives. You do not manage a server. The platform starts the function, runs as many copies as needed and stops them again. This function is the only part that knows the API key and talks to the model.

Architecture diagram: the browser loads static files from the hosting platform and sends questions to the function /api/ask; the function reads the key from an environment variable, calls the model API at the provider, receives the answer and token usage, writes logs and replies to the browser
A small AI web app: the browser talks only to your function, and only the function knows the key.

The browser never talks to the model provider directly. Anything that runs in the browser can be read by every visitor, and a key in the page would be found quickly. You can also run both parts in other places. These are the three common options:

Deploy a Small AI Web App
OptionWhat you manage yourselfA good fit when
Hosting platform with functionsYour code and the settingsSmall apps and prototypes with light to moderate traffic
Your own server with a control panelThe server, updates, security and certificatesYou already run servers or need full control
GPU cloud for your own modelThe model server as wellYou run an open-weight model instead of using a provider’s API

According to the vendors, Netlify is a platform for building, deploying and hosting websites and web apps. Plesk is a control panel for websites, DNS and databases on your own server. Runpod is a GPU cloud for running AI models, billed by usage. More options are listed in our tool comparison. We have not tested these tools.

Short descriptions from our tool comparison, based on the vendors' information — not tested by ABXK.

Step 1: Write the function

The function receives the question, checks it, calls the model and sends back only the answer. The example follows the format of Netlify Functions, which use the web-standard Request and Response objects. Other platforms use very similar code, but the file location and some settings differ.

// netlify/functions/ask.mjs
const SYSTEM_PROMPT = "You answer questions about our coworking space. ...";
const reply = (body, status = 200) => Response.json(body, { status });

export default async (req) => {
  if (req.method !== "POST") return reply({ error: "Use POST." }, 405);

  // 1. Check the input before anything costs money
  const body = await req.json().catch(() => null);
  const question = typeof body?.question === "string" ? body.question.trim() : "";
  if (!question || question.length > 1000) {
    return reply({ error: "Please ask a question of up to 1,000 characters." }, 400);
  }

  // 2. Limit requests per visitor (see step 3)

  // 3. Call the model: key and model from the environment, token limit, timeout
  const started = Date.now();
  let status = 0;
  let data = null;
  try {
    const res = await fetch("https://api.example.com/v1/chat", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.LLM_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: process.env.LLM_MODEL,
        messages: [
          { role: "system", content: SYSTEM_PROMPT },
          { role: "user", content: question },
        ],
        max_tokens: 300,
      }),
      signal: AbortSignal.timeout(20000),   // give up after 20 seconds
    });
    status = res.status;
    data = await res.json();
  } catch {
    // timeout, network error or a response that is not JSON
  }

  // 4. Check the answer (field names as in part 1; they vary by provider)
  const answer = data?.output?.[0]?.text;
  const ok = status === 200 && typeof answer === "string" && answer !== "";

  // 5. Log what you need for errors and costs, but not the question, then reply
  console.log(JSON.stringify({ status, ok, ms: Date.now() - started, usage: data?.usage }));
  if (!ok) return reply({ error: "The assistant is not available right now." }, 502);
  return reply({ answer });
};

export const config = { path: "/api/ask" };

The page calls only this function, never the provider:

const res = await fetch("/api/ask", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ question }),
});
const { answer, error } = await res.json();

Five steps of the function from left to right: check the input, limit requests, call the model with key, token limit and timeout, check the answer, log and reply; below, the rule that everything that costs money happens after the checks
What the function does on every request. A rejected request costs no tokens.

Two details matter in practice. First, error messages to the browser stay general. Details such as the provider’s error text belong in your logs, not on the screen. Second, platforms limit how long a function may run. On Netlify, a normal function may run for 60 seconds, according to its documentation. Your own timeout should be clearly shorter, so that you can still send a friendly message.

Step 2: Keep the key in an environment variable

In the settings of your hosting platform, create the environment variables LLM_API_KEY and LLM_MODEL. The function reads them at runtime, so neither value appears in your code. On Netlify, for example, you set them in the dashboard or the command line, and a changed value takes effect with the next deploy.

  • Never put the key into the repository, into frontend code, into logs or into error messages.
  • Use a separate key for this app, and separate keys for testing and production. Then you can revoke one key without stopping everything else.
  • Revoke a leaked key at once, create a new one and deploy again.

Step 3: Put brakes on costs and abuse

Your function has a public address. Anyone can call it — not only your page, but also scripts and bots — and every call costs you tokens. The OWASP Top 10 for LLM Applications 2025 lists this risk as “Unbounded Consumption”. It includes attacks that aim to drive up your bill, sometimes called “denial of wallet” (Source: OWASP Gen AI Security Project, genai.owasp.org). Several brakes in a row keep the risk small:

Six brakes as stacked rows: input length, rate limit, max tokens, timeout, a separate key and a spending limit at the provider; the numbers are examples
Six brakes against runaway bills. Each one catches what the one before lets through. The numbers are examples.

Rate limiting means a cap on how many requests one visitor may send in a given time, for example ten per minute. A serverless function may run as several separate copies that share no memory, so a simple counter in the code does not work. Use a rate limit setting of your platform, if it offers one, or count requests in a small shared database, such as a key-value store. Over the limit, the function answers with status 429, as you know from part 1. If many people use your app, a login or a bot check in front of the function helps as well. Our AI Security section covers abuse and defenses in more depth.

The last brake sits at the provider. Many model providers let you set a spending limit or alerts in their console. Set both before you share the link.

Step 4: Use your own domain with HTTPS

HTTPS encrypts the connection between the browser and your app. Browsers warn about pages without it. Most hosting platforms give each project an address with HTTPS from the start, for example a subdomain of the platform.

For your own domain, add it in the platform’s settings. The platform then shows you the DNS records to create at your domain registrar, often a CNAME or an A record. DNS is the address book of the internet: it translates a domain name into the address of a server. Changes can take some time to spread. Once the domain points to the platform, the platform requests a certificate for HTTPS, often from the nonprofit certificate authority Let’s Encrypt, and renews it automatically. Certificates expire after a set time, so automatic renewal matters. If you run your own server, check that the control panel renews certificates automatically, too.

Step 5: Watch logs and costs after launch

Every console.log line of your function appears in the platform’s function logs. Log what you need to find errors and control costs: time, status, duration, token usage, and the model and prompt version. Do not log full questions and answers by default, because they can contain personal data. If you need examples for quality checks, tell your users, keep only a few and delete them after a fixed time.

Six things to watch after launch: error rate, response time, tokens and costs, requests per sender, user feedback, and model and prompt versions
What to watch after launch. These numbers come from your own logs.

In the first weeks, look at errors and costs every day. Then set alerts, for example when the error rate jumps or the daily costs pass a limit you choose. Level 3 explains observability for LLM apps — logs, traces, dashboards and quality checks — in depth.

Launch checklist

Go through this list before you share the link, and again whenever you change the model, the prompt or the hosting:

Deploy a Small AI Web App (2)
AreaCheck
KeyOnly in environment variables; a separate key for this app; not in the repository, the page or the logs
InputEmpty and overly long questions are rejected before the model call
LimitsRate limit per visitor, max tokens and a timeout below the platform’s limit
CostsSpending limit and alerts at the provider; token usage per day visible in your logs
ErrorsFriendly messages for users; no internal details in responses
PrivacyUsers know they are talking to an AI; your privacy policy names the model provider; no full texts in the logs without a reason
DomainHTTPS works on every address; the certificate renews automatically
RollbackYou know how to go back to the previous deploy; many platforms keep earlier versions for this
OwnerOne person reads the alerts and decides what happens when something goes wrong