Documentation Index
Fetch the complete documentation index at: https://mintlify.com/vercel/ai/llms.txt
Use this file to discover all available pages before exploring further.
Express with AI SDK
Learn how to integrate the AI SDK into Express applications to create AI-powered HTTP endpoints.
Why Express?
Express is a minimal and flexible Node.js framework ideal for:
- Building AI API endpoints
- Creating microservices
- Adding AI to existing Express apps
- Simple deployment options
Prerequisites
- Node.js 18+
- Basic knowledge of Express
- Vercel AI Gateway API key
Quick Start
Create a new project:
mkdir my-ai-server
cd my-ai-server
pnpm init
Install dependencies:
pnpm add express ai
pnpm add -D @types/express tsx typescript
Configure TypeScript:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"esModuleInterop": true,
"strict": true
}
}
Set environment variables:
echo "AI_GATEWAY_API_KEY=your-api-key" > .env
Basic Text Streaming
Stream AI-generated text to clients:
import { streamText } from 'ai';
import express, { Request, Response } from 'express';
import 'dotenv/config';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req: Request, res: Response) => {
const { prompt } = req.body;
const result = streamText({
model: 'openai/gpt-4o',
prompt: prompt || 'Tell me a fun fact',
});
result.pipeUIMessageStreamToResponse(res);
});
app.listen(8080, () => {
console.log('Server running on http://localhost:8080');
});
Run the server:
Test with curl:
curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{"prompt": "Why is the sky blue?"}'
UI Message Stream
Stream messages in a format compatible with useChat:
import { streamText, convertToModelMessages, UIMessage } from 'ai';
import express, { Request, Response } from 'express';
import cors from 'cors';
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/chat', async (req: Request, res: Response) => {
const { messages }: { messages: UIMessage[] } = req.body;
const result = streamText({
model: 'openai/gpt-4o',
messages: await convertToModelMessages(messages),
});
result.pipeUIMessageStreamToResponse(res);
});
app.listen(8080, () => {
console.log('Chat server ready');
});
Text-Only Stream
Stream plain text without message formatting:
import { streamText } from 'ai';
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
app.post('/api/generate', async (req: Request, res: Response) => {
const { prompt } = req.body;
const result = streamText({
model: 'openai/gpt-4o',
prompt,
});
result.pipeTextStreamToResponse(res);
});
app.listen(8080);
Custom Data Streaming
Send custom data alongside AI responses:
import {
createUIMessageStream,
pipeUIMessageStreamToResponse,
streamText,
} from 'ai';
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req: Request, res: Response) => {
pipeUIMessageStreamToResponse({
response: res,
stream: createUIMessageStream({
execute: async ({ writer }) => {
// Send custom metadata
writer.write({ type: 'start' });
writer.write({
type: 'data-custom',
data: {
timestamp: new Date().toISOString(),
model: 'gpt-4o',
},
});
// Stream AI response
const result = streamText({
model: 'openai/gpt-4o',
prompt: req.body.prompt,
});
writer.merge(result.toUIMessageStream({ sendStart: false }));
},
}),
});
});
app.listen(8080);
Implement tools for extended functionality:
import {
streamText,
convertToModelMessages,
tool,
UIMessage,
} from 'ai';
import express, { Request, Response } from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req: Request, res: Response) => {
const { messages }: { messages: UIMessage[] } = req.body;
const result = streamText({
model: 'openai/gpt-4o',
messages: await convertToModelMessages(messages),
tools: {
getWeather: tool({
description: 'Get current weather for a location',
inputSchema: z.object({
city: z.string(),
}),
execute: async ({ city }) => {
// Simulate weather API call
const weatherData = {
city,
temperature: 72,
condition: 'Sunny',
};
return weatherData;
},
}),
calculateSum: tool({
description: 'Add two numbers',
inputSchema: z.object({
a: z.number(),
b: z.number(),
}),
execute: async ({ a, b }) => {
return { result: a + b };
},
}),
},
});
result.pipeUIMessageStreamToResponse(res);
});
app.listen(8080);
Structured Output
Generate structured JSON responses:
import { generateObject, Output } from 'ai';
import express, { Request, Response } from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
app.post('/api/extract', async (req: Request, res: Response) => {
const { text } = req.body;
const { object } = await generateObject({
model: 'openai/gpt-4o',
prompt: `Extract contact information from: ${text}`,
output: Output.object({
schema: z.object({
name: z.string(),
email: z.string().email(),
phone: z.string().optional(),
}),
}),
});
return res.json(object);
});
app.listen(8080);
Error Handling
Implement comprehensive error handling:
import { streamText } from 'ai';
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req: Request, res: Response, next: NextFunction) => {
try {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
const result = streamText({
model: 'openai/gpt-4o',
prompt,
});
result.pipeUIMessageStreamToResponse(res);
} catch (error) {
next(error);
}
});
// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Error:', err);
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
});
});
app.listen(8080);
Middleware
Rate Limiting
import rateLimit from 'express-rate-limit';
export const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later.',
});
import { limiter } from './middleware/ratelimit';
app.use('/api/', limiter);
Authentication
import { Request, Response, NextFunction } from 'express';
export function authenticate(
req: Request,
res: Response,
next: NextFunction,
) {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
import { authenticate } from './middleware/auth';
app.use('/api/', authenticate);
Production Setup
Project Structure
my-ai-server/
├── src/
│ ├── index.ts
│ ├── routes/
│ │ ├── chat.ts
│ │ └── generate.ts
│ ├── middleware/
│ │ ├── auth.ts
│ │ └── ratelimit.ts
│ └── utils/
│ └── ai.ts
├── .env
├── package.json
└── tsconfig.json
Example Routes Structure
import { Router } from 'express';
import { streamText } from 'ai';
const router = Router();
router.post('/', async (req, res) => {
const result = streamText({
model: 'openai/gpt-4o',
prompt: req.body.prompt,
});
result.pipeUIMessageStreamToResponse(res);
});
export default router;
import express from 'express';
import chatRouter from './routes/chat';
import generateRouter from './routes/generate';
const app = express();
app.use(express.json());
app.use('/api/chat', chatRouter);
app.use('/api/generate', generateRouter);
app.listen(8080);
Build and Run
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Deployment
Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8080
CMD ["npm", "start"]
Railway/Render
Both platforms support Express apps with minimal configuration:
- Connect your GitHub repository
- Set environment variables
- Deploy
Best Practices
- Use Environment Variables: Never hardcode API keys
- Implement Rate Limiting: Protect against abuse
- Add CORS: Allow frontend requests
- Handle Errors: Provide meaningful error messages
- Log Requests: Monitor usage and debug issues
- Use TypeScript: Catch errors at compile time
- Validate Input: Sanitize user input
Troubleshooting
Streaming Not Working
Ensure you’re not using compression middleware before streaming:
// Don't use compression on streaming routes
app.use('/api/static', compression());
app.post('/api/chat', async (req, res) => {
// Streaming route without compression
});
Example Repository
View the complete example: github.com/vercel/ai/examples/express
Resources