Express.js Integration
This guide covers integrating @cognipeer/agent-server with Express.js.
Installation
npm install @cognipeer/agent-server expressBasic Setup
import express from 'express';
import {
createAgentServer,
createPostgresProvider,
createExpressMiddleware,
} from '@cognipeer/agent-server';
async function main() {
// Create storage provider
const storage = createPostgresProvider({
connectionString: process.env.DATABASE_URL,
});
// Create agent server
const agentServer = createAgentServer({
basePath: '/api/agents',
storage,
allowAnonymousList: true, // a quick start with no sign-in: see "Who may list" in Request Context
swagger: {
enabled: true,
path: '/docs',
title: 'My Agent API',
},
});
// Register agents
agentServer.registerCustomAgent('echo', {
processMessage: async ({ message }) => ({
content: `Echo: ${message}`,
}),
});
// Create Express app
const app = express();
app.use(express.json());
// Connect to database
await storage.connect();
// Mount agent server middleware
app.use(createExpressMiddleware(agentServer));
// Start server
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
console.log(`Swagger UI at http://localhost:${port}/api/agents/docs`);
});
}
main().catch(console.error);With Agent SDK
import express from 'express';
import {
createAgentServer,
createPostgresProvider,
createExpressMiddleware,
} from '@cognipeer/agent-server';
import { createSmartAgent, createTool, fromLangchainModel } from '@cognipeer/agent-sdk';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
async function main() {
const storage = createPostgresProvider({
connectionString: process.env.DATABASE_URL,
});
// Create tools
const weatherTool = createTool({
name: 'get_weather',
description: 'Get the current weather for a location',
schema: z.object({
location: z.string().describe('The city name'),
}),
func: async ({ location }) => {
return { temperature: 22, conditions: 'Sunny', location };
},
});
// Create agent
const model = fromLangchainModel(new ChatOpenAI({
modelName: 'gpt-4',
}));
const assistant = createSmartAgent({
name: 'Weather Assistant',
model,
tools: [weatherTool],
systemMessage: 'You are a helpful weather assistant.',
});
// Create server
const agentServer = createAgentServer({
basePath: '/api/agents',
storage,
allowAnonymousList: true, // a quick start with no sign-in: see "Who may list" in Request Context
swagger: { enabled: true },
});
// Register SDK agent
agentServer.registerSDKAgent('weather-assistant', assistant, {
description: 'An AI assistant that can check the weather',
version: '1.0.0',
});
const app = express();
app.use(express.json());
await storage.connect();
app.use(createExpressMiddleware(agentServer));
app.listen(3000);
}With Authentication
import express from 'express';
import {
createAgentServer,
createPostgresProvider,
createTokenAuthProvider,
createExpressMiddleware,
} from '@cognipeer/agent-server';
async function main() {
const storage = createPostgresProvider({
connectionString: process.env.DATABASE_URL,
});
// Create auth provider
const authProvider = createTokenAuthProvider({
tokens: {
[process.env.API_KEY_1]: 'user-1',
[process.env.API_KEY_2]: 'user-2',
},
});
const agentServer = createAgentServer({
basePath: '/api/agents',
storage,
auth: {
enabled: true,
provider: authProvider,
excludeRoutes: ['/docs', '/docs/*'], // Public routes
},
swagger: { enabled: true },
});
// Register agents...
const app = express();
app.use(express.json());
await storage.connect();
app.use(createExpressMiddleware(agentServer));
app.listen(3000);
}With CORS
The Express adapter does not apply the server's cors setting (the Node handler, Fastify and the Next.js preflight do). Add CORS in Express: the cors package, or the middleware this package exports (it answers the preflight, allows Content-Type, Authorization and Last-Event-ID, and exposes Retry-After, Content-Disposition and Content-Length):
import express from 'express';
import {
createAgentServer,
createPostgresProvider,
createCorsMiddleware,
createExpressMiddleware,
} from '@cognipeer/agent-server';
async function main() {
const storage = createPostgresProvider({
connectionString: process.env.DATABASE_URL,
});
const agentServer = createAgentServer({
basePath: '/api/agents',
storage,
});
const app = express();
// Before the agent server, so the preflight is answered
app.use(createCorsMiddleware(['http://localhost:3000', 'https://myapp.com']));
// (or, with the cors package: app.use(cors({ origin: [...] })))
app.use(express.json());
await storage.connect();
app.use(createExpressMiddleware(agentServer));
app.listen(3000);
}See Embedding & limits for what each adapter does with cors.
Mounting on a Sub-path
const app = express();
// Mount at custom path
app.use('/v1', createExpressMiddleware(agentServer));
// Now routes are:
// GET /v1/api/agents/agents
// POST /v1/api/agents/conversations
// etc.mountOnExpress(app, agentServer) mounts the middleware at the server's own basePath for you.
A file uploaded as itself needs the URL to start with basePath
The adapter recognises POST /files?name=... with the file's own Content-Type by looking at the original URL. Behind an extra mount prefix (/v1 above) the URL does not start with basePath, so such an upload is not recognised and answers 400 VALIDATION_ERROR ("file is required"); the JSON form of the upload keeps working. If you need raw uploads, put the prefix into the server's basePath ('/v1/api/agents') and mount the middleware at the root.
Uploading Files
A file can be uploaded as itself (POST /files?name=... with the file's own Content-Type, see Files). The middleware takes req.body when it is a Buffer - mount express.raw({ type: '*/*', limit: '25mb' }) on that route if you want a body parser to do the reading - and otherwise reads the request itself (up to limits.maxBodySize, by default ~1.4 x the largest file an agent accepts). express.json() leaves such a request alone.
app.use(express.json());
app.use(createExpressMiddleware(agentServer)); // no express.raw() neededError Handling
The middleware answers its own errors in the standard envelope ({ error: { code, message, details } }, see Error Handling); an errorHandler option takes over:
app.use(createExpressMiddleware(agentServer, {
errorHandler: (error, req, res) => {
const err = error as { statusCode?: number; code?: string; message?: string };
res.status(err.statusCode || 500).json({ error: { code: err.code || 'INTERNAL_ERROR', message: err.message || 'Internal Server Error' } });
},
}));Errors in your own routes go to Express's error handler as usual.
Graceful Shutdown
Turns run independently of their HTTP connections, so a server that is shutting down decides what happens to the turns still running: wait for them, or cancel them.
const app = express();
await storage.connect();
app.use(createExpressMiddleware(agentServer));
const server = app.listen(3000);
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down...');
server.close(async () => {
// Stop the sweep and cancel the turns that are still running (the answers streamed so far are kept)
await agentServer.close({ abort: true });
await storage.disconnect();
console.log('Server closed');
process.exit(0);
});
});agentServer.close() without abort only stops the periodic sweep. A turn that was cut off by a kill is closed as interrupted by the next server that starts on the same database (see Runs).
TypeScript Configuration
Recommended tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "dist"
},
"include": ["src"]
}Troubleshooting
| Symptom | Cause and fix |
|---|---|
My own routes under the same prefix work, the agent server's 404 | The middleware passes a request on (next()) when its URL does not start with basePath. Check basePath against the real URL. |
| A browser blocks the call (CORS) | The Express adapter does not apply cors; use the cors package or createCorsMiddleware (With CORS). |
413 PAYLOAD_TOO_LARGE on an upload | The body is over limits.maxBodySize (by default about 1.4 x the largest files.maxFileSize of the agents registered at startup, plus 64 KiB). Set it when your agents are built per request. |
400 file is required on POST /files?name=... | The request is not recognised as a raw upload: it has no Content-Type, or the app is mounted behind an extra prefix (Mounting on a Sub-path). |

