Fastify is a high-performance Node.js web framework designed for building JSON APIs with minimal overhead. A content generation project uses Fastify to expose a /generate endpoint that accepts structured requests for content generation, validates them against a JSON Schema contract, streams the request to the Anthropic API, and returns an annotated response with token usage statistics and example counts. This pattern—schema-validated input, streaming LLM proxy, enriched output—is common in modern AI-powered backend services.
What makes Fastify particularly well-suited for this role is its built-in JSON Schema validation, plugin-based architecture, and first-class support for async/await. Unlike Express, Fastify compiles route schemas into optimized validation functions at startup using Ajv, which means malformed requests are rejected before they reach handler code. This eliminates an entire class of runtime errors and reduces the amount of defensive code needed inside route handlers.
The content generation server also demonstrates a practical pattern for port management: when the default port is occupied, it auto-increments to the next available port, making it possible to run multiple instances during development or testing without manual configuration. Combined with structured logging via Fastify’s built-in Pino integration, the result is a production-ready API server that handles content generation reliably.
Fastify uses JSON Schema to declare the expected shape of request bodies, query parameters, and response payloads at the route level. When a request arrives, Fastify validates it against the compiled schema before invoking the handler. If validation fails, Fastify returns a 400 response with a structured error message describing exactly which fields failed and why. This moves input validation from imperative code into declarative configuration, making route contracts self-documenting and enforceable.
The /generate endpoint acts as a proxy between the client and the Anthropic API. Rather than buffering the entire LLM response before returning it, the server can stream tokens as they arrive using server-sent events or chunked transfer encoding. This reduces time-to-first-byte for the client and avoids holding large response bodies in memory. The proxy layer also adds value by checking format availability, applying rate limits, and annotating the response with metadata the raw API does not provide.
Before forwarding a generation request to the LLM, the server validates that the requested output format (markdown, HTML, JSON, plain text) is available for the selected model and configuration. This pre-flight check prevents wasted API calls and gives the client an immediate, actionable error rather than a cryptic failure from the downstream service. The format registry is a simple map of format names to validation functions and post-processing transformers.
The Anthropic API returns token counts (input tokens, output tokens) alongside the generated content. The content generation server captures these metrics and attaches them to the response object along with application-level metadata: the number of examples found in the generated content, the format used, the model version, and the request duration. This enriched response gives clients the data they need for cost tracking, quality monitoring, and usage dashboards without requiring a separate analytics call.
When starting the server, the code attempts to bind to the configured port. If that port is already in use (EADDRINUSE), it catches the error and retries with the next port number, up to a configurable maximum number of attempts. This is particularly useful during development when multiple server instances may be running, and it avoids the common frustration of “port already in use” errors blocking the development workflow.
The Fastify server is created with logging enabled and routes are registered with JSON Schema definitions for request validation. The schema object describes required fields, types, and constraints. Fastify compiles these schemas at startup for fast runtime validation.
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
const generateSchema = {
body: {
type: 'object',
required: ['prompt', 'format'],
properties: {
prompt: { type: 'string', minLength: 1, maxLength: 100000 },
format: { type: 'string', enum: ['markdown', 'html', 'json', 'text'] },
model: { type: 'string', default: 'claude-sonnet-4-20250514' },
maxTokens: { type: 'integer', minimum: 1, maximum: 4096, default: 1024 },
temperature: { type: 'number', minimum: 0, maximum: 1, default: 0.7 },
},
additionalProperties: false,
},
response: {
200: {
type: 'object',
properties: {
content: { type: 'string' },
format: { type: 'string' },
model: { type: 'string' },
usage: {
type: 'object',
properties: {
inputTokens: { type: 'integer' },
outputTokens: { type: 'integer' },
},
},
exampleCount: { type: 'integer' },
durationMs: { type: 'integer' },
},
},
},
};
fastify.post('/generate', { schema: generateSchema }, async (request, reply) => {
const { prompt, format, model, maxTokens, temperature } = request.body;
if (!isFormatAvailable(format)) {
return reply.code(400).send({ error: `Format "${format}" is not available` });
}
const startTime = Date.now();
const result = await generateContent({ prompt, format, model, maxTokens, temperature });
const durationMs = Date.now() - startTime;
return {
content: result.content,
format,
model: result.model,
usage: {
inputTokens: result.usage.input_tokens,
outputTokens: result.usage.output_tokens,
},
exampleCount: countExamples(result.content),
durationMs,
};
});
The generateContent function wraps the Anthropic SDK, constructing messages from the prompt and format instructions. It handles API errors gracefully and extracts the response content and usage metadata into a normalized structure.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
interface GenerateOptions {
prompt: string;
format: string;
model: string;
maxTokens: number;
temperature: number;
}
interface GenerateResult {
content: string;
model: string;
usage: { input_tokens: number; output_tokens: number };
}
async function generateContent(opts: GenerateOptions): Promise<GenerateResult> {
const systemPrompt = buildSystemPrompt(opts.format);
const response = await client.messages.create({
model: opts.model,
max_tokens: opts.maxTokens,
temperature: opts.temperature,
system: systemPrompt,
messages: [{ role: 'user', content: opts.prompt }],
});
const textBlock = response.content.find((b) => b.type === 'text');
if (!textBlock) {
throw new Error('No text content in response');
}
return {
content: textBlock.text,
model: response.model,
usage: response.usage,
};
}
function buildSystemPrompt(format: string): string {
const formatInstructions: Record<string, string> = {
markdown: 'Respond in well-structured Markdown with headers, lists, and code blocks.',
html: 'Respond with valid, semantic HTML5. Use appropriate tags for structure.',
json: 'Respond with valid JSON. Use descriptive keys and proper nesting.',
text: 'Respond in plain text with clear paragraph breaks.',
};
return formatInstructions[format] ?? formatInstructions.text;
}
The server startup logic tries the configured port first, then increments on EADDRINUSE errors. This avoids manual port juggling during development while logging which port was ultimately bound.
async function startServer(basePort: number, maxRetries = 10): Promise<void> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const port = basePort + attempt;
try {
await fastify.listen({ port, host: '0.0.0.0' });
fastify.log.info(`Server listening on port ${port}`);
return;
} catch (err: any) {
if (err.code === 'EADDRINUSE') {
fastify.log.warn(`Port ${port} in use, trying ${port + 1}`);
continue;
}
throw err;
}
}
throw new Error(`Could not find open port after ${maxRetries} attempts`);
}
startServer(Number(process.env.PORT) || 3000);
After receiving the generated content, the server scans it for code blocks or structured examples and reports the count alongside token usage. This metadata helps clients assess generation quality without parsing the content themselves.
function countExamples(content: string): number {
const codeBlockPattern = /```[\s\S]*?```/g;
const matches = content.match(codeBlockPattern);
return matches ? matches.length : 0;
}
function isFormatAvailable(format: string): boolean {
const supported = new Set(['markdown', 'html', 'json', 'text']);
return supported.has(format);
}
This Fastify server pattern is the backbone of a content generation pipeline. Clients send structured prompts describing the content they need—API documentation, code examples, tutorial sections—and receive formatted output along with metadata that feeds into quality tracking dashboards. The schema validation ensures that every request conforms to the expected contract, eliminating an entire category of “garbage in, garbage out” failures that plague less disciplined API designs.
The token usage annotation is particularly valuable for cost management. By surfacing input and output token counts directly in the API response, teams can build cost attribution models that tie generation costs to specific features or user actions. The exampleCount field provides a rough quality signal: generated content with zero code examples for a technical topic likely needs regeneration, and automated pipelines can use this as a heuristic for retry decisions.
The auto-incrementing port pattern has broader applicability beyond this project. Any development tool, mock server, or sidecar process that needs to bind a port can benefit from this approach. It eliminates the “port 3000 is already in use” friction that interrupts development flow, especially in microservice environments where multiple services run locally.
Define the JSON Schema for every route before writing the handler. This schema becomes the single source of truth for input validation, response serialization, and API documentation. Fastify’s schema compilation means there is no runtime penalty for thorough validation, and tools like fastify-swagger can auto-generate OpenAPI specs from the same schema objects.
When wrapping an external API (like Anthropic), add value in the proxy layer: validate inputs against your own business rules, normalize the response shape, inject application-specific metadata, and handle errors with context-aware messages. The proxy should never simply pass through the raw upstream response—that couples your clients to a third-party contract you don’t control.
Wrap the listen() call in a retry loop that catches EADDRINUSE and increments the port. Log which port was actually bound so that other processes and tests can discover it. For production deployments, disable the fallback and fail fast—an occupied port in production usually indicates a misconfiguration that should be fixed, not worked around.
fast-json-stringify. Omitting the response schema means Fastify falls back to standard JSON.stringify, losing the performance benefit that makes Fastify faster than Express for JSON-heavy APIs.