Skip to content

Express.js Integration ​

This guide covers integrating @cognipeer/agent-server with Express.js.

Installation ​

bash
npm install @cognipeer/agent-server express

Basic Setup ​

typescript
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 ​

typescript
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 ​

typescript
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):

typescript
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 ​

typescript
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.

typescript
app.use(express.json());
app.use(createExpressMiddleware(agentServer));   // no express.raw() needed

Error Handling ​

The middleware answers its own errors in the standard envelope ({ error: { code, message, details } }, see Error Handling); an errorHandler option takes over:

typescript
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.

typescript
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:

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

Troubleshooting ​

SymptomCause and fix
My own routes under the same prefix work, the agent server's 404The 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 uploadThe 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).

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub