Edit this page on GitHub

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

ConceptFastifySetu-TS
RuntimeNode.js onlyDeno, Node.js, Bun, Cloudflare Workers
Request/ResponseFastifyRequest/FastifyReplyWeb-standard Request/Response
Plugin Systemfastify.register()app.register(plugin)
EncapsulationPer-instance decorationCapability tokens, service registry
Decoratorsfastify.decorate()Service registry
SchemaJSON 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() with createApplication()
  • Replace app.get/post/put/delete with programmatic routes
  • Replace hooks with lifecycle hooks or middleware
  • Replace decorators with service registration
  • Replace JSON Schema validation with Zod
  • Replace app.inject() with inject() from testing utilities
  • Update logging to use LoggerPlugin
  • Update error handling to use middleware
  • Update deployment for target runtime

Next Steps