Fastify Example
A multi-tenant project management API using Fastify.
Features
- Fastify plugin for tenant context
- Type-safe routes
- Project and Task management
- User relationships
- Connection retry with exponential backoff
Schema
typescript
// schema.ts
import { pgTable, uuid, varchar, timestamp, boolean, integer } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: varchar("email", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 255 }).notNull(),
active: boolean("active").default(true),
createdAt: timestamp("created_at").defaultNow(),
});
export const projects = pgTable("projects", {
id: uuid("id").primaryKey().defaultRandom(),
name: varchar("name", { length: 255 }).notNull(),
description: varchar("description", { length: 1000 }),
ownerId: uuid("owner_id").references(() => users.id),
status: varchar("status", { length: 50 }).default("active"),
createdAt: timestamp("created_at").defaultNow(),
});
export const tasks = pgTable("tasks", {
id: uuid("id").primaryKey().defaultRandom(),
title: varchar("title", { length: 255 }).notNull(),
projectId: uuid("project_id").references(() => projects.id),
assigneeId: uuid("assignee_id").references(() => users.id),
priority: integer("priority").default(0),
completed: boolean("completed").default(false),
createdAt: timestamp("created_at").defaultNow(),
});
// Type exports
export type User = typeof users.$inferSelect;
export type Project = typeof projects.$inferSelect;
export type Task = typeof tasks.$inferSelect;Configuration
typescript
// config.ts
import { defineConfig } from "drizzle-multitenant";
import * as schema from "./schema.js";
export const config = defineConfig({
connection: {
url: process.env.DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/multitenant",
pooling: {
max: 20,
idleTimeoutMillis: 30000,
},
retry: {
maxAttempts: 3,
initialDelayMs: 100,
maxDelayMs: 5000,
backoffMultiplier: 2,
jitter: true,
},
},
isolation: {
strategy: "schema",
schemaNameTemplate: (tenantId) => `tenant_${tenantId}`,
maxPools: 100,
poolTtlMs: 60 * 60 * 1000,
},
schemas: {
tenant: schema,
shared: schema,
},
hooks: {
onPoolCreated: (tenantId) => {
console.log(`[Pool] Created: ${tenantId}`);
},
},
debug: {
enabled: process.env.NODE_ENV === "development",
logQueries: true,
logPoolEvents: true,
},
});
export { schema };Application
typescript
// index.ts
import Fastify from "fastify";
import { createTenantManager, createFastifyPlugin } from "drizzle-multitenant";
import { eq } from "drizzle-orm";
import { config, schema } from "./config.js";
const fastify = Fastify({ logger: true });
// Create tenant manager
const tenants = createTenantManager(config);
// Register tenant plugin
fastify.register(createFastifyPlugin, {
manager: tenants,
extractTenantId: (request) => request.headers["x-tenant-id"] as string,
onTenantResolved: (tenantId, request) => {
request.log.info({ tenantId }, "Tenant resolved");
},
});
// Health check
fastify.get("/health", async () => {
return { status: "ok", pools: tenants.getPoolCount() };
});
// === Users Routes ===
fastify.get("/users", async (request) => {
const db = request.tenantDb!;
return db.select().from(schema.users);
});
fastify.post<{ Body: { email: string; name: string } }>("/users", async (request, reply) => {
const db = request.tenantDb!;
const { email, name } = request.body;
const [user] = await db.insert(schema.users).values({ email, name }).returning();
return reply.status(201).send(user);
});
// === Projects Routes ===
fastify.get("/projects", async (request) => {
const db = request.tenantDb!;
return db.select().from(schema.projects);
});
fastify.post<{ Body: { name: string; description?: string; ownerId?: string } }>(
"/projects",
async (request, reply) => {
const db = request.tenantDb!;
const { name, description, ownerId } = request.body;
const [project] = await db
.insert(schema.projects)
.values({ name, description, ownerId })
.returning();
return reply.status(201).send(project);
}
);
fastify.get<{ Params: { id: string } }>("/projects/:id", async (request, reply) => {
const db = request.tenantDb!;
const [project] = await db
.select()
.from(schema.projects)
.where(eq(schema.projects.id, request.params.id));
if (!project) {
return reply.status(404).send({ error: "Project not found" });
}
return project;
});
// === Tasks Routes ===
fastify.get("/tasks", async (request) => {
const db = request.tenantDb!;
return db.select().from(schema.tasks);
});
fastify.get<{ Params: { projectId: string } }>("/projects/:projectId/tasks", async (request) => {
const db = request.tenantDb!;
return db
.select()
.from(schema.tasks)
.where(eq(schema.tasks.projectId, request.params.projectId));
});
fastify.post<{
Params: { projectId: string };
Body: { title: string; assigneeId?: string; priority?: number };
}>("/projects/:projectId/tasks", async (request, reply) => {
const db = request.tenantDb!;
const { title, assigneeId, priority } = request.body;
const [task] = await db
.insert(schema.tasks)
.values({
title,
projectId: request.params.projectId,
assigneeId,
priority,
})
.returning();
return reply.status(201).send(task);
});
fastify.patch<{ Params: { id: string }; Body: { completed?: boolean; priority?: number } }>(
"/tasks/:id",
async (request, reply) => {
const db = request.tenantDb!;
const { completed, priority } = request.body;
const [task] = await db
.update(schema.tasks)
.set({ completed, priority })
.where(eq(schema.tasks.id, request.params.id))
.returning();
if (!task) {
return reply.status(404).send({ error: "Task not found" });
}
return task;
}
);
// === Admin Routes ===
fastify.get("/admin/stats", async () => {
return {
poolCount: tenants.getPoolCount(),
timestamp: new Date().toISOString(),
};
});
// Start server
const start = async () => {
try {
const port = parseInt(process.env.PORT || "3001");
await fastify.listen({ port, host: "0.0.0.0" });
console.log(`Fastify Multi-tenant API running on http://localhost:${port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
// Graceful shutdown
process.on("SIGTERM", async () => {
await fastify.close();
await tenants.dispose();
process.exit(0);
});API Endpoints
bash
# Create a user
curl -X POST http://localhost:3001/users \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: acme" \
-d '{"email": "alice@acme.com", "name": "Alice"}'
# Create a project
curl -X POST http://localhost:3001/projects \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: acme" \
-d '{"name": "Website Redesign", "description": "Q1 project"}'
# List projects
curl http://localhost:3001/projects -H "X-Tenant-ID: acme"
# Create a task
curl -X POST http://localhost:3001/projects/{projectId}/tasks \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: acme" \
-d '{"title": "Design mockups", "priority": 1}'
# List tasks for project
curl http://localhost:3001/projects/{projectId}/tasks -H "X-Tenant-ID: acme"
# Update task status
curl -X PATCH http://localhost:3001/tasks/{id} \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: acme" \
-d '{"completed": true}'
# Health check
curl http://localhost:3001/healthpackage.json
json
{
"name": "fastify-example",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "npx drizzle-multitenant migrate --all"
},
"dependencies": {
"drizzle-multitenant": "^1.0.8",
"drizzle-orm": "^0.29.0",
"fastify": "^4.24.0",
"pg": "^8.11.0"
},
"devDependencies": {
"@types/pg": "^8.10.0",
"drizzle-kit": "^0.20.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}