Migrating from Fastify
This guide helps you migrate from Fastify to Setu-TS. Setu-TS shares Fastify’s philosophy of plugin encapsulation and hooks, while adding dependency injection, TypeScript-first design, and runtime independence.
Key Differences
| Concept | Fastify | Setu-TS |
|---|---|---|
| Runtime | Node.js only | Deno, Node.js, Bun, Cloudflare Workers |
| Request/Response | FastifyRequest/FastifyReply | Web-standard Request/Response |
| Plugin System | fastify.register() | app.register(plugin) |
| Encapsulation | Per-instance decoration | Capability tokens, service registry |
| Decorators | fastify.decorate() | Service registry |
| Schema | JSON Schema (ajv) | Zod (or custom validators) |
Basic Application
Fastify
import fastify from 'fastify';
const app = fastify({ logger: true });
app.get('/', async (request, reply) => {
return { message: 'Hello' };
});
app.listen({ port: 3000 }, (err) => {
if (err) throw err;
console.log('Server listening on port 3000');
});
Setu-TS
import type { MiddlewareFunction } from '@setu-ts/common';
import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { LoggerPlugin } from '@setu-ts/logger-plugin';
const app = createApplication();
app.register(RuntimePlugin());
app.register(LoggerPlugin());
app.router.get('/', async (ctx) => {
return ctx.response.json({ message: 'Hello' });
});
await app.start({ port: 3000 });
console.log('Server listening on port 3000');
Routes
Fastify
interface Params {
id: string;
}
interface Querystring {
search?: string;
}
interface Body {
name: string;
email: string;
}
app.get<{ Params; Querystring }>('/:id', async (request, reply) => {
const { id } = request.params;
const { search } = request.query;
return { id, search };
});
app.post<{ Body }>('/users', async (request, reply) => {
const body = request.body;
return { created: body.name };
});
Setu-TS
app.router.get('/users/:id', async (ctx) => {
const id = ctx.params.id;
const search = new URL(ctx.request.url).searchParams.get('search');
return ctx.response.json({ id, search });
});
app.router.post('/users', async (ctx) => {
const body: Record<string, unknown> = await ctx.request.json();
return ctx.response.status(201).json({ created: body });
});
Route Groups
Fastify
app.register(async (fastify) => {
fastify.get('/users', () => []);
fastify.post('/users', () => ({}));
}, { prefix: '/api' });
Setu-TS
app.router.get('/api/users', async (ctx) => ctx.response.json([]));
app.router.post('/api/users', async (ctx) => ctx.response.json({}));
// Or use a route group
app.router.group('/api', (group) => {
group.get('/users', async (ctx) => ctx.response.json([]));
group.post('/users', async (ctx) => ctx.response.json({}));
});
Hooks
Fastify
// onRequest
app.addHook('onRequest', (request, reply, done) => {
console.log('onRequest');
done();
});
// preHandler
app.addHook('preHandler', (request, reply, done) => {
console.log('preHandler');
done();
});
// preSerialization
app.addHook('preSerialization', (request, reply, payload, done) => {
console.log('preSerialization');
done();
});
// onResponse
app.addHook('onResponse', (request, reply, done) => {
console.log('onResponse');
done();
});
// onSend
app.addHook('onSend', (request, reply, payload, done) => {
console.log('onSend');
done();
});
// onError
app.addHook('onError', (request, reply, error, done) => {
console.log('onError');
done();
});
Setu-TS
// Using lifecycle hooks
ctx.lifecycle.onRequest((ctx) => {
console.log('Request started:', ctx.request.url);
});
ctx.lifecycle.onResponse((ctx) => {
console.log('Response sent:', ctx.response.snapshot().status);
});
ctx.lifecycle.onError((error, ctx) => {
console.error('Request error:', error);
});
// Using middleware for transformation
const preSerializationMiddleware: MiddlewareFunction = async (ctx, next) => {
await next();
// Post-processing after response is generated
};
app.middleware.add(preSerializationMiddleware);
Middleware
Fastify
app.use((req, res, next) => {
console.log('Middleware');
next();
});
// Route-specific
app.use('/api/*', apiMiddleware);
Setu-TS
const myMiddleware: MiddlewareFunction = async (ctx, next) => {
console.log('Middleware');
await next();
};
app.middleware.add(myMiddleware);
// Route-specific middleware
// Route-specific middleware is not supported in Setu-TS; use a middleware that checks ctx.request.path instead.
Decorators
Fastify
// Register decorator
app.decorate('myUtil', {
formatDate: (date: Date) => date.toISOString(),
});
// Use decorator
app.get('/', async (request, reply) => {
return { date: app.myUtil.formatDate(new Date()) };
});
Setu-TS
// Register as a service
interface MyUtil {
formatDate(date: Date): string;
}
ctx.services.register<MyUtil>('myUtil', {
formatDate: (date: Date) => date.toISOString(),
});
// Use service
app.router.get('/', async (ctx) => {
const myUtil = ctx.services.get<MyUtil>('myUtil');
return ctx.response.json({ date: myUtil.formatDate(new Date()) });
});
Validation
Fastify
app.addSchema({
$id: 'userSchema',
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
});
app.post('/users', {
schema: {
body: 'userSchema',
},
}, async (request, reply) => {
return { created: true };
});
Setu-TS
import { z } from '@std/zod';
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
});
// Using validation plugin
app.router.post('/users', async (ctx) => {
const result = userSchema.safeParse(await ctx.request.json());
if (!result.success) {
return ctx.response.status(400).json({ errors: result.error.errors });
}
const body = result.data;
return ctx.response.status(201).json({ created: true });
});
Error Handling
Fastify
app.setErrorHandler((error, request, reply) => {
reply.status(error.statusCode || 500).send({
error: error.message,
});
});
Setu-TS
const errorMiddleware: MiddlewareFunction = async (ctx, next) => {
try {
await next();
} catch (error) {
if (error instanceof HttpException) {
return ctx.response.status(error.status).json(
{ error: error.message },
);
}
console.error('Unhandled error', { error });
return ctx.response.status(500).json(
{ error: 'Internal server error' },
);
}
};
app.middleware.add(errorMiddleware);
Type Providers
Fastify
interface FastifySchema {
body: { name: string; email: string };
}
app.post<FastifySchema>('/users', async (request, reply) => {
const body = request.body; // Typed
return { created: true };
});
Setu-TS
interface CreateUserData {
name: string;
email: string;
}
app.router.post('/users', async (ctx) => {
const body = await ctx.request.json<CreateUserData>();
return ctx.response.status(201).json({ created: true });
});
Lifecycle Hooks
Fastify
// onReady
app.addHook('onReady', async () => {
console.log('Ready!');
});
// onClose
app.addHook('onClose', async () => {
console.log('Closing');
});
Setu-TS
ctx.lifecycle.onBootstrap(() => {
console.log('Ready!');
});
ctx.lifecycle.onClose(() => {
console.log('Closing');
});
// More detailed shutdown lifecycle
ctx.lifecycle.onStopping(() => {
console.log('Stopping - no new requests');
});
ctx.lifecycle.onShutdown(() => {
console.log('Shutdown - draining requests');
});
Plugins
Fastify
async function authPlugin(fastify, options) {
fastify.decorate('authenticate', async (request) => {
if (!request.headers.authorization) {
throw new Error('Missing authorization header');
}
});
}
app.register(authPlugin, {/* options */});
Setu-TS
// Self-contained example plugin demonstrating the registration pattern.
function MyAuthPlugin(): IPlugin {
return {
name: 'my-auth',
version: '1.0.0',
async register(ctx) {
ctx.services.register('authenticate', async (request: Request) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
throw new Error('Missing authorization header');
}
});
},
};
}
app.register(MyAuthPlugin());
Server Decoration
Fastify
app.decorateRequest('user', null);
app.addHook('onRequest', async (request, reply) => {
request.user = { id: 1, name: 'John' };
});
Setu-TS
const authMiddleware: MiddlewareFunction = async (ctx, next) => {
// Set user on context
ctx.state.set('user', { id: 1, name: 'John' });
await next();
};
app.middleware.add(authMiddleware);
Async Initialization
Fastify
const app = fastify();
await app.ready();
// App is ready but not listening
Setu-TS
const app = createApplication();
app.register(RuntimePlugin());
await app.start();
// App is ready and listening (if port specified)
// Or ready for fetch (if no port)
Testing
Fastify
import Fastify from 'fastify';
const app = Fastify();
app.get('/', async () => ({ hello: 'world' }));
const response = await app.inject({
method: 'GET',
url: '/',
});
console.log(response.json());
Setu-TS
import { createTestApp, inject } from '@setu-ts/testing';
const app = await createTestApp();
app.router.get('/', async (ctx) => ctx.response.json({ hello: 'world' }));
const response = await inject(app, {
method: 'GET',
url: '/',
});
console.log(response.json());
Common Patterns
Encapsulation
Fastify
app.register(async (child) => {
child.decorate('childUtil', () => 'child');
child.get('/child', () => ({ util: child.childUtil() }));
});
// parent cannot access childUtil
Setu-TS
// Use capability tokens for encapsulation
ctx.services.register('child-util', { value: 'child' });
// Register routes in a scoped manner
ctx.router.group('/child', (group) => {
group.get('/', async (ctx) => {
const util = ctx.services.get<{ value: string }>('child-util');
return ctx.response.json({ util });
});
});
Reply Decorators
Fastify
app.decorateReply('withUser', function (user) {
this.user = user;
return this;
});
app.get('/', async (request, reply) => {
return reply.withUser({ id: 1 }).code(200);
});
Setu-TS
// Use context state
app.router.get('/', async (ctx) => {
ctx.state.set('user', { id: 1 });
return ctx.response.json({ user: ctx.state.get('user') });
});
Migration Checklist
- Replace
fastify()withcreateApplication() - Replace
app.get/post/put/deletewith programmatic routes - Replace hooks with lifecycle hooks or middleware
- Replace decorators with service registration
- Replace JSON Schema validation with Zod
- Replace
app.inject()withinject()from testing utilities - Update logging to use
LoggerPlugin - Update error handling to use middleware
- Update deployment for target runtime
Next Steps
- Getting Started - Set up your first application
- Plugin Architecture - Deep dive into plugins
- Examples - See real-world applications