All Tutorials
typescript
express
ai
openai
api

Build an AI Chatbot API with Express, OpenAI, and TypeScript

A code-along tutorial: build a conversational chatbot API with Express and TypeScript that calls the OpenAI API and keeps a short conversation history in memory. One file, one route, paste and run in about ten minutes.

10 min read2026-07-30

The short answer

You build a POST /chat endpoint with Express and TypeScript that sends a running conversation history to the OpenAI API and returns the model's reply. Each request appends the user's message to an in-memory array, calls openai.chat.completions.create with that history, and appends the assistant's reply before sending it back. The result is a chatbot that remembers what you said earlier in the conversation, without a database. You need your own OpenAI API key, and each request costs a small number of tokens. Setup takes about ten minutes once you have the key.

  • Setup time: about 10 minutes (once you have an OpenAI API key)
  • Runtime dependencies: 2 (Express, openai)
  • Route: POST /chat
  • Stack: TypeScript, Express, openai npm package, tsx
  • Cost: pay-per-token on the OpenAI API (not free after the trial credit)
  • Every file is in this page: paste into a fresh project and run

What you will build

By the end of this tutorial you will have a small Express API with one route: POST /chat. Send it a message, and it calls the OpenAI API with your full conversation history and returns the model's reply. Send another message, and it picks up where it left off. The conversation history lives in a plain in-memory array, so it resets when the server restarts and never touches a database.

That simplicity is intentional. The goal is to understand the API call, the message format, and the history pattern before adding persistence, streaming, or a frontend. Once you have this working, every common extension is a small, contained change.

Every file you need is in this page. Paste each one into a fresh project, follow the setup steps, and it runs. No repo to clone.

One thing to know before you start: this costs money

The OpenAI API charges per token, not per month. New accounts get a small trial credit, but after that each request costs a fraction of a cent. For a tutorial like this the total cost is likely under a dollar, but there is no free tier once the credit runs out.

If you want a version of this tutorial that costs nothing per request, our local LLM tutorial builds the same conversational pattern with Ollama running on your machine instead of a paid API. The code structure is similar; the trade-off is that a local model is less capable than GPT-4o for most tasks.

If the OpenAI cost is fine for you, read on.

What you need

  • Node.js 18 or newer. Run node -v to check. Update at nodejs.org if you are behind.
  • An OpenAI API key. Create one at platform.openai.com. You need a funded account or active trial credit to make API calls.
  • A terminal and a text editor. Everything else installs with npm.

Setup

Create a fresh project and install the two runtime dependencies. Express handles the HTTP routing; the official openai package wraps the OpenAI REST API with TypeScript types built in.

bash
mkdir ai-chatbot
cd ai-chatbot
npm init -y
npm install express openai
npm install -D typescript tsx @types/node @types/express

Add a tsconfig.json at the project root. These settings work with tsx in development and compile with tsc for a production build:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

Then open package.json, add "type": "module", and set these scripts:

json
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

Your API key goes in an environment variable so it never ends up in your code. The simplest approach for a local tutorial: set it in the shell before running the server. On macOS and Linux:

bash
export OPENAI_API_KEY=sk-your-key-here

Or put it in a .env file and load it with dotenv. The tutorial reads process.env.OPENAI_API_KEY, so either approach works. Create a src/ directory in the project root; the source file below goes there.

How the OpenAI chat API works

The OpenAI chat completion API takes a messages array. Each item has a role (system, user, or assistant) and a content string. The model reads the whole array and replies as the assistant.

Three roles: - system sets the personality and instructions. It is typically the first message and stays constant. - user is what you, or your user, sent. - assistant is what the model replied last time.

To keep a conversation going across multiple requests, you add each new user message to the array and each reply from the model before sending the next request. The model reads the history and responds in context. This tutorial holds that array in memory on the server.

The server

The whole project is one file. Create src/server.ts:

typescript
import express, { type Request, type Response } from "express";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";

const app = express();
app.use(express.json());

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Conversation history shared across all requests to this server.
// It resets when the server restarts -- add a database or session store to persist it.
const messages: ChatCompletionMessageParam[] = [
  {
    role: "system",
    content:
      "You are a helpful assistant. Keep your answers concise and direct.",
  },
];

app.post("/chat", async (req: Request, res: Response) => {
  const { message } = req.body ?? {};
  if (typeof message !== "string" || message.trim() === "") {
    res.status(400).json({ error: 'Send a non-empty "message" string.' });
    return;
  }

  messages.push({ role: "user", content: message });

  try {
    const completion = await client.chat.completions.create({
      model: "gpt-4o-mini",
      messages,
    });

    const reply = completion.choices[0]?.message?.content ?? "";
    messages.push({ role: "assistant", content: reply });

    res.json({ reply });
  } catch (err) {
    // Remove the user message so a retry does not double-count it.
    messages.pop();
    const message =
      err instanceof Error ? err.message : "Unknown error from OpenAI API";
    res.status(502).json({ error: message });
  }
});

const port = Number(process.env.PORT) || 3000;
app.listen(port, () => {
  console.log(`AI chatbot API listening on http://localhost:${port}`);
  console.log(`POST /chat with { "message": "your text" } to start.`);
});

A few things worth noting.

`gpt-4o-mini` is the model. It is OpenAI's fastest and cheapest chat model, which makes it the right default for a tutorial. Swap the string for gpt-4o if you want the full model; the code is otherwise identical.

The `messages` array is the conversation. Every call appends the user's message and the model's reply. On the next call, the model sees the full history and responds in context. That is all that is happening here; the OpenAI API is stateless on its side.

On error, remove the last user message. If the API call fails, the user message is already in the array. Rolling it back before returning the 502 keeps the history clean so a retry does not send a duplicated message.

Run it and test with curl

Make sure OPENAI_API_KEY is set in your shell, then start the server:

bash
npm run dev
# -> AI chatbot API listening on http://localhost:3000

In a second terminal, send a message:

bash
curl -X POST http://localhost:3000/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "What is a closure in JavaScript?"}'
# -> {"reply":"A closure is a function that retains access to variables..."}

Send a follow-up that only makes sense in context, to verify the history is working:

bash
curl -X POST http://localhost:3000/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Can you give me a short code example of that?"}'
# -> {"reply":"Sure! Here's a simple closure example:..."}

The second reply references the first question without you repeating it. That is the history at work.

Go deeper on AI engineering

Want to understand what is happening inside these API calls? ChatGPT Prompt Engineering for Developers (DeepLearning.AI + OpenAI) is a free two-hour course that covers system prompts, few-shot examples, and how to shape model output. Our AI engineer roadmap maps the free courses in order if you want the full learning path.

What to build next

You have a working chatbot API in about 50 lines. A few natural next steps.

Add streaming. Right now the response arrives all at once after the model finishes generating. OpenAI supports server-sent events (SSE) for streaming tokens as they arrive. Change stream: true in the completion call and use for await (const chunk of completion) to write chunks to the response as they come in.

Add a system prompt configuration. Accept an optional systemPrompt field in the request body and let callers set the assistant's personality per session. Useful when you want one backend to power multiple chatbot experiences.

Persist the history. Move the messages array into a session store or a database, keyed by a session id the client sends. Then each user gets their own conversation that survives a server restart.

Build a frontend. A simple HTML form with a <textarea> and a fetch call to POST /chat is enough to make this browser-usable. Add a small Express static handler, or wire it into a Next.js app.

For prompt design and the broader context of building AI products, our guide on becoming an AI engineer covers the free resources in order. And if you want to skip the per-token cost entirely, the local LLM tutorial builds the same pattern with Ollama running on your machine.

Frequently asked questions

Do I have to pay for the OpenAI API?

Yes, after your trial credit runs out. OpenAI charges per token, not per month. For a tutorial like this you are looking at a few cents total, but there is no permanently free tier. If you want a chatbot that costs nothing per request after setup, see our local LLM tutorial at /tutorials/run-a-local-llm-with-nodejs, which uses Ollama on your machine instead.

Can I use a different model provider, like Anthropic or Google?

Yes. The message format (role + content) is the same across most providers. For Anthropic's Claude, install the @anthropic-ai/sdk package and use client.messages.create instead of client.chat.completions.create. The conversation history shape is slightly different but the concept is identical. This tutorial uses the openai package because it is the most widely used and the pattern generalizes well.

Why does the history reset when I restart the server?

The messages array lives in memory on the server process. When the process stops, the array is gone. To persist conversations, store the messages in a database (Postgres, SQLite, Redis) keyed by a session id. The db.ts pattern from our URL shortener tutorial at /tutorials/build-url-shortener-express-typescript-sqlite shows one clean way to add SQLite persistence.

How do I keep the conversation from growing forever?

Each message you send costs tokens, and the model has a context window limit (a maximum number of tokens it can read at once). For a short tutorial session this is fine, but in a real app you would trim the history: keep the last N messages, or summarize older messages into a single context block. A simple approach is to slice the array before the API call: messages.slice(-20) keeps the 20 most recent turns.

Keep going on FreeCodingCourses