Building Agent-Ready APIs with Express and Fastify
The third framework tutorial in our series (after Next.js and Django/Flask). Eight code patterns for Express and four for Fastify that take a typical Node.js API from a score of 10-20 out of 100 to Silver (60+) or Gold (75+). Every pattern includes copy-paste code and its scoring impact.
Why Most Node.js APIs Score Under 20
Express is the most popular Node.js framework with over 30 million weekly npm downloads. Fastify is the fastest-growing alternative at 4 million weekly downloads. Together they power the majority of Node.js APIs on the internet. Yet most score under 20 on agent readiness.
The reason is not technical limitation — both frameworks are fully capable of serving agent-ready APIs. The problem is that the default Express/Fastify setup produces an API that is invisible to agents: no OpenAPI spec, HTML error pages, no discovery files, no rate limit headers, and no health endpoint.
The good news: fixing this requires no architecture changes. You add middleware, plugins, and a few static routes. An afternoon of work covers 80% of the Agent Readiness Score weight.
8 Express Patterns for Agent Readiness
Each pattern below includes the npm packages needed, copy-paste code, and the exact scoring impact across our 9 scoring dimensions. Implement them in order — the first three cover 60% of the total impact.
1. OpenAPI via swagger-jsdoc + swagger-ui-express
+12 points (D1 +6, D2 +6)Auto-generate an OpenAPI 3.0 spec from JSDoc comments on your routes. This is the single highest-value change — agents use OpenAPI to discover what your API can do. Covers D1 Discovery and D2 API Quality.
// npm install swagger-jsdoc swagger-ui-express
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const spec = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
},
apis: ['./routes/*.js'],
});
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec));
app.get('/openapi.json', (req, res) => res.json(spec));2. Structured Error Middleware
+10 points (D2 +3, D8 +4, D9 +3)Replace Express default HTML error pages with JSON. Every error returns { error, code, request_id }. Agents need machine-readable errors to retry intelligently. Covers D2, D8, and D9.
const { v4: uuidv4 } = require('uuid');
// Attach request ID to every request
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// Structured error handler (must be last middleware)
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
error: err.message || 'Internal server error',
code: err.code || 'INTERNAL_ERROR',
request_id: req.id,
...(status === 422 && err.details
? { details: err.details }
: {}),
});
});3. /health Endpoint
+5 points (D8 +5)A simple GET /health that returns { status: "ok", timestamp, version }. Agents check this before making real requests. Monitoring services use it. Covers D8 Reliability.
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0',
uptime: process.uptime(),
});
});4. Static agent-card.json
+8 points (D1 +5, D9 +3)Serve a JSON file at /.well-known/agent-card.json that describes your API to agents. Name, description, capabilities, auth requirements, endpoint URL. This is how agents discover you. Covers D1 and D9.
const agentCard = {
name: 'My Business API',
description: 'API for product catalog and ordering',
url: 'https://api.example.com',
version: '1.0.0',
capabilities: {
tools: ['search_products', 'get_pricing', 'create_order'],
auth: { type: 'bearer', header: 'Authorization' },
docs: 'https://api.example.com/docs',
openapi: 'https://api.example.com/openapi.json',
},
};
app.get('/.well-known/agent-card.json', (req, res) => {
res.json(agentCard);
});5. swagger-jsdoc Route Annotations
+6 points (D2 +4, D6 +2)Annotate each route handler with JSDoc that swagger-jsdoc converts to OpenAPI operations. Describe parameters, request bodies, responses, and error codes. Agents read these to understand each endpoint.
/**
* @openapi
* /products/{id}:
* get:
* summary: Get product by ID
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Product found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Product'
* 404:
* description: Product not found
*/
app.get('/products/:id', async (req, res) => {
const product = await db.getProduct(req.params.id);
if (!product) {
return res.status(404).json({
error: 'Product not found',
code: 'NOT_FOUND',
request_id: req.id,
});
}
res.json(product);
});6. Security Headers with Helmet
+5 points (D7 +5)One line of code adds Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, and more. Agents and scanners check these headers. Covers D7 Security.
const helmet = require('helmet');
// Add security headers to all responses
app.use(helmet());
// If you serve an OpenAPI spec or agent card,
// allow framing from your docs domain:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
frameSrc: ["'self'", 'https://docs.example.com'],
},
},
}));7. Rate Limiting with X-RateLimit Headers
+6 points (D8 +3, D9 +3)Use express-rate-limit and expose X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Agents use these to throttle requests and avoid 429 errors. Covers D8 and D9.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100,
standardHeaders: true, // X-RateLimit-* headers
legacyHeaders: false,
message: {
error: 'Rate limit exceeded',
code: 'RATE_LIMITED',
retry_after: 60,
},
});
app.use('/api/', limiter);8. Bearer Auth with Passport
+6 points (D7 +4, D3 +2)Use passport-http-bearer for token auth. Agents send Authorization: Bearer <token> and get structured 401 responses if invalid. Covers D7 Security and D3 Onboarding.
const passport = require('passport');
const BearerStrategy = require('passport-http-bearer');
passport.use(new BearerStrategy(async (token, done) => {
const user = await db.findByToken(token);
if (!user) return done(null, false);
return done(null, user);
}));
// Protected route
app.get('/api/orders',
passport.authenticate('bearer', { session: false }),
async (req, res) => {
const orders = await db.getOrders(req.user.id);
res.json({ data: orders, total: orders.length });
}
);Fastify Equivalents: Schema-First Advantage
Fastify has a structural advantage for agent readiness: its schema-first approach means route validation and OpenAPI documentation come from the same source. You define a JSON Schema for each route, and Fastify uses it for both request validation and spec generation. This eliminates the gap between what your API accepts and what your docs describe.
@fastify/swagger
Fastify generates OpenAPI specs from route schemas automatically. No JSDoc comments needed — the schema you already write for validation becomes your API documentation.
await fastify.register(require('@fastify/swagger'), {
openapi: {
info: { title: 'My API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
},
});
await fastify.register(require('@fastify/swagger-ui'), {
routePrefix: '/docs',
});Custom Error Handler Plugin
Fastify error handlers get the request object. Return structured JSON with request_id, error code, and field-level validation details from Ajv.
fastify.setErrorHandler((error, request, reply) => {
const status = error.statusCode || 500;
reply.status(status).send({
error: error.message,
code: error.code || 'INTERNAL_ERROR',
request_id: request.id, // Fastify auto-generates
...(error.validation
? { details: error.validation }
: {}),
});
});Built-in Health Check
Fastify has no built-in health route, but adding one takes 5 lines. Combine with @fastify/under-pressure for memory and event loop monitoring.
await fastify.register(require('@fastify/under-pressure'), {
maxEventLoopDelay: 1000,
maxHeapUsedBytes: 200 * 1024 * 1024,
exposeStatusRoute: '/health',
});Schema Validation = Documentation
Fastify validates request/response with JSON Schema. The same schemas power OpenAPI docs. Write once, get validation + documentation + agent-readable specs.
fastify.get('/products/:id', {
schema: {
params: {
type: 'object',
properties: { id: { type: 'string' } },
required: ['id'],
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
price: { type: 'number' },
},
},
},
},
}, async (req) => {
return db.getProduct(req.params.id);
});Total Scoring Impact: From Under 20 to Silver or Gold
Here is the cumulative impact of each pattern. Implementing all eight takes a typical Express or Fastify API from 10-20 to 66-76 — solidly in Silver or Gold territory.
Priority order: If you can only implement three patterns, choose OpenAPI spec (+12), structured errors (+10), and agent-card.json (+8). These three changes alone add 30 points and cover the highest-weighted scoring dimensions. The remaining five patterns are incremental improvements that push you from Bronze into Silver and Gold.
Frequently Asked Questions
Should I use Express or Fastify for agent-ready APIs?
Both work. Fastify has an edge because its schema-first design means route validation and OpenAPI documentation come from the same source. Express requires swagger-jsdoc annotations as a separate step. For new projects, Fastify saves time. For existing Express apps, adding the 8 patterns described here is straightforward and does not require migration.
How much time does this take to implement?
For an existing Express or Fastify API, adding all 8 patterns takes 2-4 hours. The highest-value changes (OpenAPI spec, structured errors, agent-card.json) can be done in under an hour and cover 60% of the scoring impact. Health check and security headers take minutes each.
Do I need all 8 patterns to reach Silver?
No. OpenAPI + structured errors + agent-card.json + health check gets you to approximately 45-55 points (Bronze). Adding rate limit headers, auth, and security headers pushes past 60 into Silver. The patterns are additive — each one lifts your score independently.
What about llms.txt? How does that fit in?
llms.txt is a plain text file at /llms.txt that describes your API in natural language for LLMs. It complements OpenAPI (which is structured) with human-readable context. Adding it takes 10 minutes and adds approximately 4 points to D1 Discovery. See our llms.txt guide for the format.
How does this compare to the Next.js and Django/Flask tutorials?
Same scoring principles, different framework patterns. Next.js uses route handlers and middleware. Django/Flask use DRF and flask-smorest. Express/Fastify use their own middleware and plugin ecosystems. The 9 scoring dimensions do not care about your framework — they measure what agents see when they interact with your API.
See your API score before and after
Run a free Agent Readiness Scan on your Express or Fastify API. Implement the patterns above, then scan again to see the improvement.