Fastify Integration Guide
Build high-performance AI APIs with Fastify and NeuroLink
Overview
Fastify is a high-performance Node.js web framework focused on providing the best developer experience with minimal overhead. This guide shows how to integrate NeuroLink with Fastify to create blazing-fast, production-ready AI endpoints with type-safe schema validation, plugin architecture, and built-in logging.
Key Features
- 🚀 High Performance: Up to 2x faster than Express with minimal overhead
- 📋 Schema Validation: Built-in TypeBox/JSON Schema validation
- 🔌 Plugin Architecture: Encapsulated, reusable components
- 🔒 Authentication: JWT with @fastify/jwt, API key decorators
- ⚡ Rate Limiting: @fastify/rate-limit with Redis support
- 📊 Built-in Logging: Pino logger out of the box
- 🔄 Streaming: Native SSE and WebSocket via @fastify/websocket
What You'll Build
- Type-safe AI API with Fastify and TypeBox
- Plugin-based authentication system
- Rate-limited endpoints with Redis
- Response caching with hooks
- Streaming chat endpoints (SSE and WebSocket)
- Production monitoring with Pino and Prometheus
Quick Start
1. Initialize Project
mkdir my-ai-api
cd my-ai-api
npm init -y
npm install fastify @juspay/neurolink dotenv
npm install @fastify/type-provider-typebox @sinclair/typebox
npm install -D @types/node typescript ts-node
2. Setup TypeScript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
3. Create Basic Server
// src/index.ts
import Fastify from "fastify";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { Type, Static } from "@sinclair/typebox";
import { NeuroLink } from "@juspay/neurolink";
import dotenv from "dotenv";
dotenv.config();
// Initialize Fastify with TypeBox type provider
const app = Fastify({
logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();
// Initialize NeuroLink
const ai = new NeuroLink({
providers: [
{
name: "openai",
config: { apiKey: process.env.OPENAI_API_KEY },
},
{
name: "anthropic",
config: { apiKey: process.env.ANTHROPIC_API_KEY },
},
],
});
// Request schema with TypeBox
const GenerateSchema = {
body: Type.Object({
prompt: Type.String({ minLength: 1, maxLength: 10000 }),
provider: Type.Optional(Type.String()),
model: Type.Optional(Type.String()),
}),
};
type GenerateBody = Static<typeof GenerateSchema.body>;
// Basic endpoint with schema validation
app.post<{ Body: GenerateBody }>(
"/api/generate",
{ schema: GenerateSchema },
async (request, reply) => {
const { prompt, provider = "openai", model = "gpt-4o-mini" } = request.body;
const result = await ai.generate({
input: { text: prompt },
provider,
model,
});
return {
content: result.content,
usage: result.usage,
cost: result.cost,
};
},
);
// Start server
const start = async () => {
try {
const PORT = parseInt(process.env.PORT || "3000", 10);
await app.listen({ port: PORT, host: "0.0.0.0" });
console.log(`AI API server running on http://localhost:${PORT}`);
} catch (error) {
app.log.error(error);
process.exit(1);
}
};
start();
4. Environment Variables
# .env
PORT=3000
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_AI_API_KEY=AIza...
5. Run Server
npx ts-node src/index.ts
6. Test API
curl -X POST http://localhost:3000/api/generate \
-H "Content-Type: application/json" \
-d '{"prompt": "Explain AI in one sentence"}'
Authentication
API Key Authentication with Decorators
// src/plugins/api-key-auth.ts
import fp from "fastify-plugin";
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
declare module "fastify" {
interface FastifyInstance {
apiKeyAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
}
}
async function apiKeyAuthPlugin(fastify: FastifyInstance) {
fastify.decorate(
"apiKeyAuth",
async function (request: FastifyRequest, reply: FastifyReply) {
const apiKey = request.headers["x-api-key"] as string;
if (!apiKey) {
reply.code(401).send({ error: "API key is required" });
return;
}
if (apiKey !== process.env.API_SECRET) {
reply.code(401).send({ error: "Invalid API key" });
return;
}
},
);
}
export default fp(apiKeyAuthPlugin, { name: "api-key-auth" });
// src/index.ts
import apiKeyAuthPlugin from "./plugins/api-key-auth";
await app.register(apiKeyAuthPlugin);
// Protected endpoint
app.post(
"/api/generate",
{ preHandler: [app.apiKeyAuth], schema: GenerateSchema },
async (request, reply) => {
// ... AI generation
},
);
JWT Authentication with @fastify/jwt
npm install @fastify/jwt
// src/plugins/jwt-auth.ts
import fp from "fastify-plugin";
import fastifyJwt from "@fastify/jwt";
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
declare module "@fastify/jwt" {
interface FastifyJWT {
payload: { userId: string; username: string };
user: { userId: string; username: string };
}
}
declare module "fastify" {
interface FastifyInstance {
authenticate: (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void>;
}
}
async function jwtAuthPlugin(fastify: FastifyInstance) {
await fastify.register(fastifyJwt, {
secret: process.env.JWT_SECRET || "supersecret",
sign: { expiresIn: "24h" },
});
fastify.decorate(
"authenticate",
async function (request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();
} catch (error) {
reply.code(401).send({ error: "Invalid or expired token" });
}
},
);
}
export default fp(jwtAuthPlugin, { name: "jwt-auth" });
// Login endpoint
app.post("/api/auth/login", async (request, reply) => {
const { username, password } = request.body as any;
if (username === "admin" && password === "password") {
const token = app.jwt.sign({ userId: "123", username });
return { token, expiresIn: "24h" };
}
reply.code(401).send({ error: "Invalid credentials" });
});
// Protected endpoint
app.post(
"/api/generate",
{ preHandler: [app.authenticate] },
async (request, reply) => {
const user = request.user;
// ... AI generation
},
);
Rate Limiting
@fastify/rate-limit Plugin
npm install @fastify/rate-limit
// src/plugins/rate-limit.ts
import fp from "fastify-plugin";
import rateLimit from "@fastify/rate-limit";
import { FastifyInstance } from "fastify";
async function rateLimitPlugin(fastify: FastifyInstance) {
await fastify.register(rateLimit, {
max: 100,
timeWindow: "1 minute",
errorResponseBuilder: (request, context) => ({
error: "Too Many Requests",
message: `Rate limit exceeded. Try again in ${Math.round(context.ttl / 1000)} seconds.`,
statusCode: 429,
}),
keyGenerator: (request) =>
(request.headers["x-api-key"] as string) ||
request.user?.userId ||
request.ip,
});
}
export default fp(rateLimitPlugin, { name: "rate-limit" });
// Route-specific rate limit
app.post(
"/api/analyze",
{
config: {
rateLimit: { max: 10, timeWindow: "1 minute" },
},
},
async (request, reply) => {
// Expensive AI operation
},
);
Redis-Based Custom Rate Limiting
npm install @fastify/rate-limit ioredis
// src/plugins/redis-rate-limit.ts
import fp from "fastify-plugin";
import rateLimit from "@fastify/rate-limit";
import Redis from "ioredis";
import { FastifyInstance } from "fastify";
async function redisRateLimitPlugin(fastify: FastifyInstance) {
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
await fastify.register(rateLimit, {
global: true,
max: 100,
timeWindow: "1 minute",
redis: redis,
nameSpace: "rate-limit:",
skipOnError: true,
});
fastify.addHook("onClose", async () => {
await redis.quit();
});
}
export default fp(redisRateLimitPlugin, { name: "redis-rate-limit" });
Response Caching
Redis Caching with Hooks
npm install ioredis
// src/plugins/cache.ts
import fp from "fastify-plugin";
import Redis from "ioredis";
import { createHash } from "crypto";
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
declare module "fastify" {
interface FastifyInstance {
cache: Redis;
cacheResponse: (ttl: number) => {
onRequest: (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void>;
onSend: (
request: FastifyRequest,
reply: FastifyReply,
payload: string,
) => Promise<string>;
};
}
interface FastifyRequest {
cacheKey?: string;
}
}
async function cachePlugin(fastify: FastifyInstance) {
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
fastify.decorate("cache", redis);
fastify.decorate("cacheResponse", (ttl: number = 3600) => ({
onRequest: async (request: FastifyRequest, reply: FastifyReply) => {
const keyData = { url: request.url, body: request.body };
request.cacheKey = `ai:${createHash("sha256")
.update(JSON.stringify(keyData))
.digest("hex")}`;
const cached = await redis.get(request.cacheKey);
if (cached) {
reply.header("X-Cache", "HIT");
reply.send(JSON.parse(cached));
}
},
onSend: async (
request: FastifyRequest,
reply: FastifyReply,
payload: string,
) => {
if (request.cacheKey && reply.statusCode === 200) {
await redis.setex(request.cacheKey, ttl, payload);
}
return payload;
},
}));
fastify.addHook("onClose", async () => {
await redis.quit();
});
}
export default fp(cachePlugin, { name: "cache" });
// Cached endpoint
const cacheHooks = app.cacheResponse(3600);
app.post(
"/api/generate",
{
onRequest: cacheHooks.onRequest,
onSend: cacheHooks.onSend,
},
async (request, reply) => {
const result = await ai.generate({
input: { text: request.body.prompt },
});
return { content: result.content, usage: result.usage };
},
);
Streaming Responses
Server-Sent Events (SSE) with reply.raw
// src/routes/stream.ts
import { FastifyInstance } from "fastify";
import { Type, Static } from "@sinclair/typebox";
const StreamSchema = {
body: Type.Object({
prompt: Type.String({ minLength: 1 }),
provider: Type.Optional(Type.String()),
}),
};
type StreamBody = Static<typeof StreamSchema.body>;
export default async function streamRoutes(fastify: FastifyInstance) {
fastify.post<{ Body: StreamBody }>(
"/stream",
{ schema: StreamSchema },
async (request, reply) => {
const { prompt, provider = "openai" } = request.body;
// Set SSE headers using reply.raw
reply.raw.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
try {
for await (const chunk of fastify.ai.stream({
input: { text: prompt },
provider,
})) {
reply.raw.write(
`data: ${JSON.stringify({ content: chunk.content })}\n\n`,
);
}
reply.raw.write("data: [DONE]\n\n");
reply.raw.end();
} catch (error: any) {
reply.raw.write(
`data: ${JSON.stringify({ error: error.message })}\n\n`,
);
reply.raw.end();
}
},
);
}
WebSocket with @fastify/websocket
npm install @fastify/websocket
// src/routes/websocket.ts
import { FastifyInstance } from "fastify";
export default async function websocketRoutes(fastify: FastifyInstance) {
fastify.get("/ws", { websocket: true }, (socket, request) => {
request.log.info("WebSocket client connected");
socket.on("message", async (rawData: Buffer) => {
try {
const { prompt, provider = "openai" } = JSON.parse(rawData.toString());
socket.send(JSON.stringify({ type: "start" }));
for await (const chunk of fastify.ai.stream({
input: { text: prompt },
provider,
})) {
socket.send(
JSON.stringify({ type: "chunk", content: chunk.content }),
);
}
socket.send(JSON.stringify({ type: "done" }));
} catch (error: any) {
socket.send(JSON.stringify({ type: "error", error: error.message }));
}
});
socket.on("close", () => {
request.log.info("WebSocket client disconnected");
});
});
}
// src/index.ts
import websocket from "@fastify/websocket";
import websocketRoutes from "./routes/websocket";
await app.register(websocket);
await app.register(websocketRoutes);
Production Patterns
Pattern 1: Plugin Architecture
// src/plugins/neurolink.ts
import fp from "fastify-plugin";
import { NeuroLink } from "@juspay/neurolink";
import { FastifyInstance } from "fastify";
declare module "fastify" {
interface FastifyInstance {
ai: NeuroLink;
}
}
async function neuroLinkPlugin(
fastify: FastifyInstance,
options: { providers: Array<{ name: string; config: Record<string, any> }> },
) {
const ai = new NeuroLink({ providers: options.providers });
fastify.decorate("ai", ai);
fastify.log.info("NeuroLink initialized");
}
export default fp(neuroLinkPlugin, { name: "neurolink" });
// src/index.ts
import neuroLinkPlugin from "./plugins/neurolink";
await app.register(neuroLinkPlugin, {
providers: [
{ name: "openai", config: { apiKey: process.env.OPENAI_API_KEY } },
{ name: "anthropic", config: { apiKey: process.env.ANTHROPIC_API_KEY } },
],
});
// Now use app.ai anywhere
app.post("/api/generate", async (request, reply) => {
const result = await app.ai.generate({
input: { text: request.body.prompt },
});
return { content: result.content };
});
Pattern 2: Usage Tracking with Hooks
// src/plugins/usage-tracking.ts
import fp from "fastify-plugin";
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
async function usageTrackingPlugin(fastify: FastifyInstance) {
fastify.addHook(
"onSend",
async (request: FastifyRequest, reply: FastifyReply, payload: string) => {
if (reply.statusCode === 200) {
try {
const response = JSON.parse(payload);
if (response.usage) {
await fastify.cache.lpush(
`usage:${request.user?.userId || "anonymous"}`,
JSON.stringify({
tokens: response.usage.totalTokens,
cost: response.cost,
timestamp: new Date(),
}),
);
}
} catch (error) {
// Ignore non-JSON responses
}
}
return payload;
},
);
}
export default fp(usageTrackingPlugin, { name: "usage-tracking" });
Pattern 3: Error Handler with setErrorHandler
// src/plugins/error-handler.ts
import fp from "fastify-plugin";
import { FastifyInstance, FastifyError, FastifyReply } from "fastify";
async function errorHandlerPlugin(fastify: FastifyInstance) {
fastify.setErrorHandler(
async (error: FastifyError, request, reply: FastifyReply) => {
request.log.error({ error: error.message }, "Request error");
if (error.message.includes("rate limit") || error.statusCode === 429) {
return reply.code(429).send({
error: "Rate Limit Exceeded",
message: "Too many requests. Please try again later.",
});
}
if (error.message.includes("quota")) {
return reply.code(503).send({
error: "Service Quota Exceeded",
message: "AI service quota exceeded.",
});
}
if (error.validation) {
return reply.code(400).send({
error: "Validation Error",
details: error.validation,
});
}
return reply.code(error.statusCode || 500).send({
error: "Internal Server Error",
message:
process.env.NODE_ENV === "development"
? error.message
: "Something went wrong",
});
},
);
}
export default fp(errorHandlerPlugin, { name: "error-handler" });
Schema Validation
TypeBox Schema Definitions
// src/schemas/ai.ts
import { Type, Static } from "@sinclair/typebox";
export const ProviderSchema = Type.Union([
Type.Literal("openai"),
Type.Literal("anthropic"),
Type.Literal("google-ai"),
]);
export const GenerateRequestSchema = Type.Object({
prompt: Type.String({ minLength: 1, maxLength: 100000 }),
provider: Type.Optional(ProviderSchema),
model: Type.Optional(Type.String()),
maxTokens: Type.Optional(Type.Integer({ minimum: 1, maximum: 128000 })),
temperature: Type.Optional(Type.Number({ minimum: 0, maximum: 2 })),
});
export type GenerateRequest = Static<typeof GenerateRequestSchema>;
export const GenerateResponseSchema = Type.Object({
content: Type.String(),
provider: Type.String(),
model: Type.String(),
usage: Type.Object({
inputTokens: Type.Integer(),
outputTokens: Type.Integer(),
totalTokens: Type.Integer(),
}),
cost: Type.Optional(Type.Number()),
});
export const ErrorResponseSchema = Type.Object({
error: Type.String(),
message: Type.String(),
details: Type.Optional(Type.Any()),
});
Route with Full Schema Validation
// src/routes/ai.ts
import { FastifyInstance } from "fastify";
import {
GenerateRequestSchema,
GenerateResponseSchema,
GenerateRequest,
ErrorResponseSchema,
} from "../schemas/ai";
export default async function aiRoutes(fastify: FastifyInstance) {
fastify.post<{ Body: GenerateRequest }>(
"/generate",
{
schema: {
body: GenerateRequestSchema,
response: {
200: GenerateResponseSchema,
400: ErrorResponseSchema,
429: ErrorResponseSchema,
},
},
preHandler: [fastify.authenticate],
},
async (request, reply) => {
const {
prompt,
provider = "openai",
model,
maxTokens,
temperature,
} = request.body;
const result = await fastify.ai.generate({
input: { text: prompt },
provider,
model,
maxTokens,
temperature,
});
return {
content: result.content,
provider: result.provider,
model: result.model,
usage: result.usage,
cost: result.cost,
};
},
);
}
Validation Options
// src/index.ts
const app = Fastify({
logger: true,
ajv: {
customOptions: {
removeAdditional: "all",
coerceTypes: true,
useDefaults: true,
allErrors: true,
},
},
}).withTypeProvider<TypeBoxTypeProvider>();
Monitoring and Logging
Pino Logger (Built-in)
// src/index.ts
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || "info",
transport:
process.env.NODE_ENV === "development"
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
redact: ["req.headers.authorization", "req.headers['x-api-key']"],
},
});
// Log AI operations
app.post("/api/generate", async (request, reply) => {
const startTime = Date.now();
request.log.info(
{ prompt: request.body.prompt.slice(0, 50) },
"AI request started",
);
const result = await app.ai.generate({
input: { text: request.body.prompt },
});
request.log.info(
{
provider: result.provider,
tokens: result.usage.totalTokens,
duration: Date.now() - startTime,
},
"AI request completed",
);
return result;
});
Prometheus Metrics
npm install prom-client
// src/plugins/metrics.ts
import fp from "fastify-plugin";
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import {
Registry,
Counter,
Histogram,
collectDefaultMetrics,
} from "prom-client";
async function metricsPlugin(fastify: FastifyInstance) {
const register = new Registry();
collectDefaultMetrics({ register });
const httpRequestsTotal = new Counter({
name: "http_requests_total",
help: "Total HTTP requests",
labelNames: ["method", "route", "status"],
registers: [register],
});
const aiRequestsTotal = new Counter({
name: "ai_requests_total",
help: "Total AI requests",
labelNames: ["provider", "model"],
registers: [register],
});
const aiRequestDuration = new Histogram({
name: "ai_request_duration_seconds",
help: "AI request duration",
labelNames: ["provider", "model"],
registers: [register],
});
fastify.addHook(
"onResponse",
async (request: FastifyRequest, reply: FastifyReply) => {
httpRequestsTotal.inc({
method: request.method,
route: request.routeOptions?.url || request.url,
status: reply.statusCode,
});
},
);
fastify.get("/metrics", async (request, reply) => {
reply.header("Content-Type", register.contentType);
return register.metrics();
});
fastify.decorate("metrics", { aiRequestsTotal, aiRequestDuration });
}
export default fp(metricsPlugin, { name: "metrics" });
Best Practices
1. Use Plugin Architecture for Modularity
// src/app.ts
export async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({ logger: true }).withTypeProvider<TypeBoxTypeProvider>();
await app.register(errorHandlerPlugin);
await app.register(metricsPlugin);
await app.register(jwtAuthPlugin);
await app.register(rateLimitPlugin);
await app.register(cachePlugin);
await app.register(neuroLinkPlugin, { providers: [...] });
await app.register(authRoutes, { prefix: "/api/auth" });
await app.register(aiRoutes, { prefix: "/api" });
return app;
}
2. Leverage TypeBox for Type Safety
app.post<{ Body: RequestBody }>(
"/api/generate",
{ schema: { body: RequestSchema } },
async (request) => {
// request.body is fully typed
const { prompt, options } = request.body;
},
);
3. Use Hooks for Cross-Cutting Concerns
app.addHook("onRequest", async (request) => {
request.startTime = Date.now();
});
app.addHook("onResponse", async (request, reply) => {
const duration = Date.now() - request.startTime;
request.log.info({ duration }, "Request completed");
});
4. Implement Graceful Shutdown
const signals = ["SIGINT", "SIGTERM"];
for (const signal of signals) {
process.on(signal, async () => {
await app.close();
process.exit(0);
});
}
5. Validate Environment at Startup
import { Type } from "@sinclair/typebox";
import Ajv from "ajv";
const ConfigSchema = Type.Object({
OPENAI_API_KEY: Type.String({ minLength: 1 }),
JWT_SECRET: Type.String({ minLength: 32 }),
});
const ajv = new Ajv({ coerceTypes: true });
if (!ajv.validate(ConfigSchema, process.env)) {
throw new Error("Configuration validation failed");
}
Deployment
Docker Deployment
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
RUN adduser -S fastify
USER fastify
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --spider -q http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# docker-compose.yml
version: "3.8"
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Production Checklist
- Environment variables validated at startup
- Rate limiting configured with Redis backend
- JWT authentication implemented
- Schema validation on all endpoints
- Comprehensive error handling with setErrorHandler
- Pino logging with appropriate log levels
- Prometheus metrics exposed at /metrics
- Response caching enabled for expensive operations
- Graceful shutdown implemented
- Health check endpoint available
- CORS configured properly (@fastify/cors)
- Request size limits configured
Related Documentation
- API Reference - NeuroLink SDK
- Express Integration - Compare with Express patterns
- Compliance Guide - Security and authentication
- Cost Optimization - Reduce costs
- Monitoring Guide - Observability
Additional Resources
- Fastify Documentation - Official Fastify docs
- TypeBox Documentation - JSON Schema type builder
- Fastify Ecosystem - Official plugins
- Pino Logger - Fastify's built-in logger
Need Help? Join our GitHub Discussions or open an issue.