npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  1. Installation
  2. Features
  3. Code Generation
  4. Setup
  5. Controller
  6. Service
  7. Middleware
  8. Error Handling
  9. Worker
  10. Registries
  11. Testing
  12. Contributing
  13. License

Installation

Install the package via npm or pnpm:

npm install @csi-foxbyte/fastify-toab
# or using pnpm
pnpm add @csi-foxbyte/fastify-toab

Features

  • 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.env before server startup
  • CLI for scaffolding boilerplate code

Code Generation

Create a new project scaffold:

pnpm fastify-toab create

Add 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 ExportUser

Note: 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 dev

Important config fields:

  • env: TypeBox schema used to validate process.env before 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.ts and before TOAB registers configured Fastify plugins.
  • onReady: Runs after fastify.ready().
  • rootDir: Source root that contains your generated @internals files and instrumentation.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:

  • request and reply
  • path: The matched request path without the querystring
  • ctx: The merged context from global, controller, and route middlewares
  • services
  • signal: Aborts when the client disconnects
  • typed body, params, querystring, and headers when 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:

  • GenericRouteError
  • isGenericError
  • genericRouteErrorHandler
  • FastifyToabRouteErrorContext
  • FastifyToabRouteErrorHandler

Registries

Registries are auto-generated indexes of your controllers, services, and workers. They are written into src/@internals/.

pnpm fastify-toab rebuild

Key generated files:

  • src/@internals/registries.ts: Creates and caches the service, worker, and controller registries.
  • src/@internals/index.ts: Exposes inferred helper types and get... accessors for generated services and workers.
  • src/@internals/run.ts: Bootstraps startServer(...) 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 test

Ensure your generated code passes the existing test suite and add new tests for custom logic.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Commit your changes with clear messages
  4. Open a pull request and describe the feature / bugfix

Please follow the repository’s code style guidelines.

License

LGPL3.0 © CSI Foxbyte