Telaio
Server

Routing

File-based route autoloading, TypeBox schema integration, request hooks, and the built-in health endpoint.

Routing

Telaio does not invent a new routing API. Fastify route syntax is unchanged. What Telaio adds is autoloading from a src/routes/ directory, typed request decorators from built-in hooks, and schema helpers that emit proper $ref pointers in the OpenAPI spec.

Accessing the Fastify instance

The app.fastify property exposes the full FastifyInstance. Everything Fastify supports -- plugins, decorators, hooks, route registration -- is available through it.

const app = await createApp({ config }).withDatabase().build();

// Register an additional plugin after build
app.fastify.register(myPlugin);

// Register a route directly
app.fastify.get('/ping', async () => ({ pong: true }));

Autoload

When you call .withPlugins({ autoload: ... }), Telaio registers @fastify/autoload over your src/routes/ directory. Every file that exports a default Fastify plugin is treated as a route module.

src/routes/
  index.ts             → GET /
  users/
    index.ts           → GET /users
    _id/
      index.ts         → GET /users/:id
  posts/
    index.ts           → GET /posts

When routeParams: true is set in the autoload options, directory names wrapped in underscores (e.g. _id) are turned into route parameters (:id). Without that flag, use [id] directory naming instead.

AutoloadOptions

OptionTypeDescription
dirstringRoot directory for route discovery. Default: src/routes/
routeParamsbooleanConvert _param directory names to :param route parameters
cascadeHooksbooleanApply hooks from _hooks.ts files to child routes
autoHooksbooleanEnable _hooks.ts auto-hook discovery

Route file shape

// src/routes/users/index.ts → GET /users
import type { FastifyPluginAsync } from 'fastify';
import { Type } from '@sinclair/typebox';
import { AutoRef, Paginated, SortPaginationParamsSchema } from 'telaio/schema';

const plugin: FastifyPluginAsync = async (fastify) => {
  fastify.get(
    '/',
    {
      schema: {
        querystring: AutoRef(SortPaginationParamsSchema),
        response: {
          200: Paginated(UserSchema),
        },
      },
    },
    async (req) => {
      const { limit, skip, sort } = req.query;
      // ...
    },
  );
};

export default plugin;
// src/routes/users/_id/index.ts → GET /users/:id (with routeParams: true)
import type { FastifyPluginAsync } from 'fastify';
import { Type } from '@sinclair/typebox';
import { AutoRef } from 'telaio/schema';

const plugin: FastifyPluginAsync = async (fastify) => {
  fastify.get(
    '/',
    {
      schema: {
        params: Type.Object({ id: Type.String() }),
        response: {
          200: AutoRef(UserSchema),
        },
      },
    },
    async (req) => {
      const { id } = req.params;
      // ...
    },
  );
};

export default plugin;

Auto-hooks

When cascadeHooks: true is set, a _hooks.ts file in any route directory registers Fastify hooks that apply to all routes in that directory and all subdirectories below it.

// src/routes/_hooks.ts — applies to every route in the app
import type { FastifyPluginAsync } from 'fastify';

const hooks: FastifyPluginAsync = async (fastify) => {
  fastify.addHook('onRequest', async (req, reply) => {
    // Runs on every request in this directory and below
  });
};

export default hooks;
// src/routes/admin/_hooks.ts — applies only to /admin/** routes
import type { FastifyPluginAsync } from 'fastify';

const hooks: FastifyPluginAsync = async (fastify) => {
  fastify.addHook('onRequest', async (req, reply) => {
    if (!req.hasAuthSession()) {
      return reply.code(401).send({ status: 'error', code: 'UNAUTHORIZED', message: 'Authentication required' });
    }
  });
};

export default hooks;

Request decorators

Telaio's built-in hooks register several decorators on every request object.

DecoratorTypeDescription
req.startTimenumberRequest start timestamp (dayjs().valueOf())
req.maybeAuthSessionTSession | nullAuthenticated session or null
req.getAuthSession()TSessionReturns the session or throws UnauthorizedError
req.hasAuthSession()booleantrue if a session is present
fastify.get('/me', async (req) => {
  const session = req.getAuthSession(); // throws 401 if not authenticated
  return { userId: session.userId };
});

fastify.get('/optional', async (req) => {
  if (req.hasAuthSession()) {
    // personalized response
  } else {
    // anonymous response
  }
});

Health endpoint

GET /healthz is registered automatically by Telaio. It returns { status: 'ok' } and is excluded from request logging. No configuration is needed.

curl http://localhost:4001/healthz
# { "status": "ok" }

Using AutoRef in schema.response

AutoRef(Schema) emits a JSON Schema $ref pointer using the schema's $id. When combined with .withSwagger(), each response schema appears in the OpenAPI spec as a named component rather than being inlined.

import { AutoRef, Paginated, UserSchema } from '../schemas/index.js';

fastify.get('/', {
  schema: {
    response: {
      200: Paginated(UserSchema),      // emits $ref for both the list and meta
      404: AutoRef(NotFoundResponseSchema),
    },
  },
}, handler);

For AutoRef to resolve correctly, the referenced schema must be registered with Fastify before the route is loaded. Telaio registers built-in schemas and schemas from src/schemas/ as part of the build assembly, before autoloading routes.

On this page