@csi-foxbyte/fastify-toab
v0.2.0-rc.4
Published
Fastify Toab is a lightweight architectural layer for Fastify that introduces a clean, structured way to organize applications using controllers and services — inspired by frameworks like trpc.
Readme
Fastify TOAB (Typed OpenAPI + BullMQ)
Fastify TOAB is a plugin for Fastify that provides strongly-typed OpenAPI route generation and seamless BullMQ worker integration. It comes with CLI code generation to scaffold controllers, services, middleware, and workers.
Table of Contents
- Installation
- Features
- Code Generation
- Setup
- Controller
- Service
- Middleware
- Error Handling
- Worker
- Registries
- Testing
- Contributing
- License
Installation
Install the package via npm or pnpm:
npm install @csi-foxbyte/fastify-toab
# or using pnpm
pnpm add @csi-foxbyte/fastify-toabFeatures
- Typed Controllers & Routes with built-in OpenAPI support
- Service Container for dependency injection
- Middleware creation with shared context
- BullMQ Worker registration and queue integration
- Built-in Fastify integrations for
@fastify/cors,@fastify/helmet,@fastify/swagger,@fastify/swagger-ui,@fastify/rate-limit,@fastify/multipart, and@fastify/under-pressure - Environment schema validation against
process.envbefore server startup - CLI for scaffolding boilerplate code
Code Generation
Create a new project scaffold:
pnpm fastify-toab createAdd a service, controller, middleware, worker, or sandboxed worker to an existing project:
pnpm fastify-toab add service User
pnpm fastify-toab add controller User
pnpm fastify-toab add middleware Auth
pnpm fastify-toab add worker User DeleteUser
pnpm fastify-toab add sandboxedWorker User ExportUserNote: Workers are generated under
<service>/workers.
Setup
Configure TOAB with a fastify-toab.config.ts file:
import { Type } from "@sinclair/typebox";
import { defineConfig } from "@csi-foxbyte/fastify-toab";
export default defineConfig({
rootDir: "src",
env: Type.Object({
PORT: Type.String({ default: "5000" }),
HOST: Type.Optional(Type.String()),
}),
globalMiddlewares: [],
fastify: {
cors: {
enabled: true,
},
helmet: {
enabled: true,
},
swagger: {
enabled: true,
openapi: {
info: {
title: "My API",
version: "1.0.0",
},
},
},
swaggerUi: {
enabled: true,
routePrefix: "/docs",
},
rateLimit: {
enabled: false,
},
multipart: {
enabled: true,
},
underPressure: {
enabled: true,
},
},
plugins: [],
server: {
fastify: {
listen: {
host: "0.0.0.0",
port: Number(process.env.PORT ?? 5000),
},
},
},
onPreStart: async (fastify, registries) => {
// register hooks, dashboards, health checks, etc.
},
onReady: async (fastify, registries) => {
fastify.log.info("Server ready");
},
});Run the generated project:
pnpm fastify-toab rebuild
pnpm fastify-toab build
pnpm fastify-toab devImportant config fields:
- env: TypeBox schema used to validate
process.envbefore the server starts. - plugins: Additional Fastify plugins to register before TOAB.
- globalMiddlewares: Middleware chain that runs before controller and route middlewares.
- fastify.cors: Built-in registration for
@fastify/cors. - fastify.helmet: Built-in registration for
@fastify/helmet. - fastify.swagger: Built-in registration for
@fastify/swagger. - fastify.swaggerUi: Built-in registration for
@fastify/swagger-ui. - fastify.rateLimit: Built-in registration for
@fastify/rate-limit. - fastify.multipart: Built-in registration for
@fastify/multipart. - fastify.underPressure: Built-in registration for
@fastify/under-pressure. - includeGenericErrorResponses: Adds the built-in generic error schemas to generated OpenAPI responses.
- onRouteError: Central hook for custom route error handling.
- logLevel: Logger level forwarded to the generated runner.
- logSerializers: Custom logger serializers for Fastify/Pino.
- prefix: Route prefix for the registered TOAB plugin.
- rolldown: Advanced bundler overrides for generated builds.
- server.fastify.listen: Options forwarded to
fastify.listen(...). - server.disableWorkers: Starts the app without initializing BullMQ workers.
- server.spawn: Spawn options used by the generated runner.
- onPreStart: Runs after
instrumentation.tsand before TOAB registers configured Fastify plugins. - onReady: Runs after
fastify.ready(). - rootDir: Source root that contains your generated
@internalsfiles andinstrumentation.ts.
Built-in Fastify plugins are registered automatically from the fastify section of the config. The current pre-integrated plugins are:
@fastify/cors@fastify/helmet@fastify/swagger@fastify/swagger-ui@fastify/rate-limit@fastify/multipart@fastify/under-pressure
If you register one of these plugins manually again through plugins, startup fails intentionally to avoid duplicate registrations.
Environment variables are validated before boot using the TypeBox schema from config.env. On validation errors, TOAB prints the failing keys and exits before Fastify starts.
If you want to register the runtime plugin manually instead of using the generated runner, import the named export:
import { fastifyToab } from "@csi-foxbyte/fastify-toab";To make the global middleware context and environment variables available in types automatically, add an ambient .d.ts file that imports your config. For example:
import type { Static, TSchema } from "@sinclair/typebox";
import config from "./fastify-toab.config.js";
type GlobalMiddlewares = typeof config extends {
globalMiddlewares?: infer Middlewares;
}
? NonNullable<Middlewares>
: [];
type EnvSchema = typeof config extends {
env: infer Envs extends TSchema;
}
? Envs
: never;
type EnvVariables = Static<EnvSchema>;
declare global {
interface FastifyToabGlobals {
globalMiddlewares: GlobalMiddlewares;
}
namespace NodeJS {
interface ProcessEnv extends EnvVariables {}
}
}
export {};With this file in place, the context produced by globalMiddlewares is inferred from fastify-toab.config.ts and becomes the default ctx type for createController(). The same file can also extend process.env from your env schema.
Controller
Controllers define HTTP routes in a typed manner. Example:
import { createController } from "@csi-foxbyte/fastify-toab";
import { authMiddleware } from "../auth/auth.middleware.js";
export const userController = createController()
.use(authMiddleware)
.rootPath("/user");
userController
.addRoute("GET", "/test")
.use(async ({ ctx }, next) => {
const nextCtx = { ...ctx, requestId: crypto.randomUUID() };
await next({ ctx: nextCtx });
return nextCtx;
})
.handler(async ({ ctx, request, reply }) => {
return { message: "Hallo Welt!", requestId: ctx.requestId };
});Each controller must be exported and registered via the generated registries.
Supported route methods are GET, HEAD, POST, DELETE, PUT, PATCH, ALL, and SSE.
Middlewares can be attached at controller level via controller.use(...) or per route via addRoute(...).use(...).
All middleware levels extend the same handler context and are merged in this order:
- global middlewares from
fastify-toab.config.ts - controller middlewares from
controller.use(...) - route middlewares from
addRoute(...).use(...)
If multiple middlewares write the same key, the later middleware overrides that key in the resulting ctx.
Route handlers receive:
requestandreplypath: The matched request path without the querystringctx: The merged context from global, controller, and route middlewaresservicessignal: Aborts when the client disconnects- typed
body,params,querystring, andheaderswhen schemas are declared
Path params are strict. If a route path contains named params such as /:id or /:id/:postId, you must declare .params(Type.Object(...)) before .handler(...).
import { createController } from "@csi-foxbyte/fastify-toab";
import { Type } from "@sinclair/typebox";
const fileController = createController().rootPath("/files");
fileController
.addRoute("GET", "/:id")
.params(Type.Object({ id: Type.String() }))
.handler(async ({ params, path }) => {
params.id;
path;
return { id: params.id };
});
fileController
.addRoute("GET", "/*")
.handler(async ({ path }) => {
return { path };
});SSE routes use the SSE method and return an AsyncIterable:
import { createController } from "@csi-foxbyte/fastify-toab";
import { Type } from "@sinclair/typebox";
const eventsController = createController().rootPath("/events");
eventsController
.addRoute("SSE", "/stream")
.output(Type.Object({ message: Type.String() }))
.handler(async function* ({ signal }) {
while (!signal.aborted) {
yield { message: "ping" };
await new Promise((resolve) => setTimeout(resolve, 1000));
}
});Service
Services encapsulate business logic and can depend on other services.
import {
createService,
InferService,
ServiceContainer,
} from "@csi-foxbyte/fastify-toab";
export const userService = createService("user", async () => {
// implement your service methods here
return {
getSession: async () => {
/* ... */
},
// etc.
};
});
export type UserService = InferService<typeof userService>;
export function getUserService(deps: ServiceContainer): Promise<UserService> {
return deps.get(userService.name);
}- createService: Define a new service namespace.
- InferService: Type helper to infer the service interface.
- ServiceContainer: Async DI container for accessing services.
Inside request-scoped services you can access the current Fastify request and reply via getRequestContext():
import { createService, getRequestContext } from "@csi-foxbyte/fastify-toab";
export const auditService = createService(
"audit",
async () => {
const { request } = getRequestContext();
return {
getRequestId() {
return request.id;
},
};
},
{ scope: "REQUEST" },
);Middleware
Middleware allows injecting shared context into routes.
import { createMiddleware, GenericRouteError } from "@csi-foxbyte/fastify-toab";
import { getAuthService } from "./auth.service.js";
export const authMiddleware = createMiddleware(
async ({ ctx, services }, next) => {
const auth = await getAuthService(services);
const session = await auth.getSession();
if (!session) {
throw new GenericRouteError(
"UNAUTHORIZED",
"User must be authenticated",
{ session },
);
}
// extend context with session
await next({ ctx: { ...ctx, session } });
},
);- createMiddleware: Wraps route handlers to provide shared logic and context.
- GenericRouteError: Optional standardized error shape when you choose to use it.
- Middlewares can be reused globally in
fastify-toab.config.ts, on controllers via.use(...), or per route via.addRoute(...).use(...).
Example with all three middleware levels:
import { Type } from "@sinclair/typebox";
import { createController, createMiddleware, defineConfig } from "@csi-foxbyte/fastify-toab";
const authMiddleware = createMiddleware(async ({ ctx }, next) => {
const nextCtx = { ...ctx, session: { userId: "123" } };
await next({ ctx: nextCtx });
return nextCtx;
});
const controllerMiddleware = createMiddleware(async ({ ctx }, next) => {
const nextCtx = { ...ctx, canReadUsers: true };
await next({ ctx: nextCtx });
return nextCtx;
});
const routeMiddleware = createMiddleware(async ({ ctx }, next) => {
const nextCtx = { ...ctx, requestId: crypto.randomUUID() };
await next({ ctx: nextCtx });
return nextCtx;
});
export default defineConfig({
env: Type.Object({}),
globalMiddlewares: [authMiddleware],
});
const userController = createController()
.use(controllerMiddleware)
.rootPath("/user");
userController
.addRoute("GET", "/me")
.use(routeMiddleware)
.handler(async ({ ctx }) => {
ctx.session.userId;
ctx.canReadUsers;
ctx.requestId;
return ctx;
});Worker
Workers process background jobs using BullMQ.
import {
createWorker,
QueueContainer,
WorkerContainer,
} from "@csi-foxbyte/fastify-toab";
import { Job } from "bullmq";
export const deleteUserWorker = createWorker()
.queue("deleteUser-queue")
.job<Job<{ userId: string }, void>>()
.connection({
/* BullMQ connection options */
})
.processor(async (job, { services, workers, queues }) => {
// process job.data.userId
return;
});
export function getDeleteUserWorker(deps: WorkerContainer) {
return deps.get(deleteUserWorker.queueName);
}
export function getDeleteUserWorkerQueue(deps: QueueContainer) {
return deps.get(deleteUserWorker.queueName);
}- createWorker: Starts a worker builder.
- queue: Sets the queue name.
- job: Defines job data and return types.
- connection: Configures Redis/BullMQ connection.
- processor: Job handler function.
Error Handling
TOAB can either rethrow route errors to Fastify or handle them centrally with onRouteError.
import {
defineConfig,
genericRouteErrorHandler,
type FastifyToabRouteErrorHandler,
} from "@csi-foxbyte/fastify-toab";
import { Type } from "@sinclair/typebox";
const onRouteError: FastifyToabRouteErrorHandler = async (ctx) => {
if (ctx.reply.sent) return;
return genericRouteErrorHandler(ctx);
};
export default defineConfig({
env: Type.Object({}),
includeGenericErrorResponses: true,
onRouteError,
});Available exports for custom error handling:
GenericRouteErrorisGenericErrorgenericRouteErrorHandlerFastifyToabRouteErrorContextFastifyToabRouteErrorHandler
Registries
Registries are auto-generated indexes of your controllers, services, and workers. They are written into src/@internals/.
pnpm fastify-toab rebuildKey generated files:
src/@internals/registries.ts: Creates and caches the service, worker, and controller registries.src/@internals/index.ts: Exposes inferred helper types andget...accessors for generated services and workers.src/@internals/run.ts: BootstrapsstartServer(...)with your config and instrumentation module.
Your project should also provide an src/instrumentation.ts default export:
import type { InstrumentationInput } from "@csi-foxbyte/fastify-toab";
export default async function instrumentation({
fastify,
registries,
}: InstrumentationInput) {
// optional startup wiring
}Testing
For examples and integration tests, refer to the tests/ directory:
pnpm testEnsure your generated code passes the existing test suite and add new tests for custom logic.
Contributing
- Fork the repository
- Create a feature branch
- Commit your changes with clear messages
- Open a pull request and describe the feature / bugfix
Please follow the repository’s code style guidelines.
License
LGPL3.0 © CSI Foxbyte
