{"author":{"@type":"Person","name":"Alexander Bock","url":"https://abxk.ai/en/autor/alexander-bock/"},"categories":["AI Development"],"content":{"markdown":"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**.\n\nThis 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](/en/ai-development/learn/your-first-llm-api-call/) and on the parts of an AI app from [Level 0](/en/ai-development/learn/how-an-ai-app-works/).\n\n## The architecture: a static page and one function\n\nA small AI web app needs only two parts of its own:\n\n- 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.\n- 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.\n\n![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](/images/learn/en/deploy-a-small-ai-web-app-architecture.webp \"A small AI web app: the browser talks only to your function, and only the function knows the key.\")\n\nThe 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:\n\n| Option | What you manage yourself | A good fit when |\n|---|---|---|\n| Hosting platform with functions | Your code and the settings | Small apps and prototypes with light to moderate traffic |\n| Your own server with a control panel | The server, updates, security and certificates | You already run servers or need full control |\n| GPU cloud for your own model | The model server as well | You run an open-weight model instead of using a provider's API |\n\nAccording 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](/en/ai-development/#area-infrastructure). We have not tested these tools.\n\n{{\u003c tools \"netlify plesk runpod\" \u003e}}\n\n## Step 1: Write the function\n\nThe 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.\n\n```javascript\n// netlify/functions/ask.mjs\nconst SYSTEM_PROMPT = \"You answer questions about our coworking space. ...\";\nconst reply = (body, status = 200) =\u003e Response.json(body, { status });\n\nexport default async (req) =\u003e {\n  if (req.method !== \"POST\") return reply({ error: \"Use POST.\" }, 405);\n\n  // 1. Check the input before anything costs money\n  const body = await req.json().catch(() =\u003e null);\n  const question = typeof body?.question === \"string\" ? body.question.trim() : \"\";\n  if (!question || question.length \u003e 1000) {\n    return reply({ error: \"Please ask a question of up to 1,000 characters.\" }, 400);\n  }\n\n  // 2. Limit requests per visitor (see step 3)\n\n  // 3. Call the model: key and model from the environment, token limit, timeout\n  const started = Date.now();\n  let status = 0;\n  let data = null;\n  try {\n    const res = await fetch(\"https://api.example.com/v1/chat\", {\n      method: \"POST\",\n      headers: {\n        \"Authorization\": `Bearer ${process.env.LLM_API_KEY}`,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        model: process.env.LLM_MODEL,\n        messages: [\n          { role: \"system\", content: SYSTEM_PROMPT },\n          { role: \"user\", content: question },\n        ],\n        max_tokens: 300,\n      }),\n      signal: AbortSignal.timeout(20000),   // give up after 20 seconds\n    });\n    status = res.status;\n    data = await res.json();\n  } catch {\n    // timeout, network error or a response that is not JSON\n  }\n\n  // 4. Check the answer (field names as in part 1; they vary by provider)\n  const answer = data?.output?.[0]?.text;\n  const ok = status === 200 \u0026\u0026 typeof answer === \"string\" \u0026\u0026 answer !== \"\";\n\n  // 5. Log what you need for errors and costs, but not the question, then reply\n  console.log(JSON.stringify({ status, ok, ms: Date.now() - started, usage: data?.usage }));\n  if (!ok) return reply({ error: \"The assistant is not available right now.\" }, 502);\n  return reply({ answer });\n};\n\nexport const config = { path: \"/api/ask\" };\n```\n\nThe page calls only this function, never the provider:\n\n```javascript\nconst res = await fetch(\"/api/ask\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ question }),\n});\nconst { answer, error } = await res.json();\n```\n\n![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](/images/learn/en/deploy-a-small-ai-web-app-function.webp \"What the function does on every request. A rejected request costs no tokens.\")\n\nTwo 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.\n\n## Step 2: Keep the key in an environment variable\n\nIn 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.\n\n- **Never** put the key into the repository, into frontend code, into logs or into error messages.\n- **Use a separate key** for this app, and separate keys for testing and production. Then you can revoke one key without stopping everything else.\n- **Revoke a leaked key at once**, create a new one and deploy again.\n\n{{\u003c callout type=\"try\" title=\"Check that the key stays hidden\" \u003e}}\nOpen your deployed page and press F12 to open the developer tools. Go to the Network tab and send a question. The request must go to `/api/ask`, not to the model provider. Then search the page source and the network requests for the first characters of your key. You should find them nowhere.\n{{\u003c /callout \u003e}}\n\n## Step 3: Put brakes on costs and abuse\n\nYour 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:\n\n![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](/images/learn/en/deploy-a-small-ai-web-app-brakes.webp \"Six brakes against runaway bills. Each one catches what the one before lets through. The numbers are examples.\")\n\n**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](/en/ai-security/) covers abuse and defenses in more depth.\n\nThe 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.\n\n## Step 4: Use your own domain with HTTPS\n\n**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.\n\nFor 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.\n\n## Step 5: Watch logs and costs after launch\n\nEvery `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.\n\n![Six things to watch after launch: error rate, response time, tokens and costs, requests per sender, user feedback, and model and prompt versions](/images/learn/en/deploy-a-small-ai-web-app-monitor.webp \"What to watch after launch. These numbers come from your own logs.\")\n\nIn 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](/en/ai-development/learn/observability-for-llm-apps/) — logs, traces, dashboards and quality checks — in depth.\n\n## Launch checklist\n\nGo through this list before you share the link, and again whenever you change the model, the prompt or the hosting:\n\n| Area | Check |\n|---|---|\n| Key | Only in environment variables; a separate key for this app; not in the repository, the page or the logs |\n| Input | Empty and overly long questions are rejected before the model call |\n| Limits | Rate limit per visitor, max tokens and a timeout below the platform's limit |\n| Costs | Spending limit and alerts at the provider; token usage per day visible in your logs |\n| Errors | Friendly messages for users; no internal details in responses |\n| Privacy | Users know they are talking to an AI; your privacy policy names the model provider; no full texts in the logs without a reason |\n| Domain | HTTPS works on every address; the certificate renews automatically |\n| Rollback | You know how to go back to the previous deploy; many platforms keep earlier versions for this |\n| Owner | One person reads the alerts and decides what happens when something goes wrong |","text":"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.\nThis 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.\nThe architecture: a static page and one function A small AI web app needs only two parts of its own:\nThe 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. 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:\nDeploy a Small AI Web App Option What you manage yourself A good fit when Hosting platform with functions Your code and the settings Small apps and prototypes with light to moderate traffic Your own server with a control panel The server, updates, security and certificates You already run servers or need full control GPU cloud for your own model The model server as well You run an open-weight model instead of using a provider\u0026rsquo;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.\nNetlifyInfrastructure \u0026amp; Hosting · Software with AI featuresPlatform for building, deploying and hosting websites and web apps. PleskInfrastructure \u0026amp; Hosting · Software with AI featuresHosting control panel for websites, WordPress, email, DNS and databases. RunpodInfrastructure \u0026amp; Hosting · AI at the coreGPU cloud for training and running AI models, billed by usage. Short descriptions from our tool comparison, based on the vendors' information — not tested by ABXK.\nStep 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.\n// netlify/functions/ask.mjs const SYSTEM_PROMPT = \u0026#34;You answer questions about our coworking space. ...\u0026#34;; const reply = (body, status = 200) =\u0026gt; Response.json(body, { status }); export default async (req) =\u0026gt; { if (req.method !== \u0026#34;POST\u0026#34;) return reply({ error: \u0026#34;Use POST.\u0026#34; }, 405); // 1. Check the input before anything costs money const body = await req.json().catch(() =\u0026gt; null); const question = typeof body?.question === \u0026#34;string\u0026#34; ? body.question.trim() : \u0026#34;\u0026#34;; if (!question || question.length \u0026gt; 1000) { return reply({ error: \u0026#34;Please ask a question of up to 1,000 characters.\u0026#34; }, 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(\u0026#34;https://api.example.com/v1/chat\u0026#34;, { method: \u0026#34;POST\u0026#34;, headers: { \u0026#34;Authorization\u0026#34;: `Bearer ${process.env.LLM_API_KEY}`, \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34;, }, body: JSON.stringify({ model: process.env.LLM_MODEL, messages: [ { role: \u0026#34;system\u0026#34;, content: SYSTEM_PROMPT }, { role: \u0026#34;user\u0026#34;, 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 \u0026amp;\u0026amp; typeof answer === \u0026#34;string\u0026#34; \u0026amp;\u0026amp; answer !== \u0026#34;\u0026#34;; // 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: \u0026#34;The assistant is not available right now.\u0026#34; }, 502); return reply({ answer }); }; export const config = { path: \u0026#34;/api/ask\u0026#34; }; The page calls only this function, never the provider:\nconst res = await fetch(\u0026#34;/api/ask\u0026#34;, { method: \u0026#34;POST\u0026#34;, headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;application/json\u0026#34; }, body: JSON.stringify({ question }), }); const { answer, error } = await res.json(); 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\u0026rsquo;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.\nStep 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.\nNever 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. ▸Check that the key stays hidden\nOpen your deployed page and press F12 to open the developer tools. Go to the Network tab and send a question. The request must go to /api/ask, not to the model provider. Then search the page source and the network requests for the first characters of your key. You should find them nowhere.\nStep 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 \u0026ldquo;Unbounded Consumption\u0026rdquo;. It includes attacks that aim to drive up your bill, sometimes called \u0026ldquo;denial of wallet\u0026rdquo; (Source: OWASP Gen AI Security Project, genai.owasp.org). Several brakes in a row keep the risk small:\nSix 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.\nThe 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.\nStep 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.\nFor your own domain, add it in the platform\u0026rsquo;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\u0026rsquo;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.\nStep 5: Watch logs and costs after launch Every console.log line of your function appears in the platform\u0026rsquo;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.\nWhat 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.\nLaunch checklist Go through this list before you share the link, and again whenever you change the model, the prompt or the hosting:\nDeploy a Small AI Web App (2) Area Check Key Only in environment variables; a separate key for this app; not in the repository, the page or the logs Input Empty and overly long questions are rejected before the model call Limits Rate limit per visitor, max tokens and a timeout below the platform\u0026rsquo;s limit Costs Spending limit and alerts at the provider; token usage per day visible in your logs Errors Friendly messages for users; no internal details in responses Privacy Users know they are talking to an AI; your privacy policy names the model provider; no full texts in the logs without a reason Domain HTTPS works on every address; the certificate renews automatically Rollback You know how to go back to the previous deploy; many platforms keep earlier versions for this Owner One person reads the alerts and decides what happens when something goes wrong ","words":1845},"date":"2026-10-14","description":"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.","image":"https://abxk.ai/images/cover/deploy-a-small-ai-web-app.webp","json":"https://abxk.ai/en/ai-development/learn/deploy-a-small-ai-web-app/index.json","kind":"page","language":"en","learning_path":{"category":"ai-development","level":1,"level_name":"Beginner"},"modified":"2026-10-14","related_tools":["Netlify","Plesk","Runpod"],"section":"ai-development","tags":["deployment","serverless","api-keys","rate-limiting","beginner"],"title":"Deploy a Small AI Web App","translations":[{"json":"https://abxk.ai/de/ai-development/learn/deploy-a-small-ai-web-app/index.json","language":"de","url":"https://abxk.ai/de/ai-development/learn/deploy-a-small-ai-web-app/"}],"url":"https://abxk.ai/en/ai-development/learn/deploy-a-small-ai-web-app/"}