Skip to content

Examples

How to use together with @fastify/swagger

This code sets up a Fastify web server with Swagger documentation and Zod-based type validation. This Fastify server:

  • Uses Zod for input validation.
  • Automatically generates OpenAPI documentation from Zod schemas.
  • Exposes a /login POST endpoint with request body validation.
  • Provides Swagger UI at /documentation.
ts
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import type { ZodTypeProvider } from '@marcalexiei/fastify-type-provider-zod';
import {
  jsonSchemaTransform,
  serializerCompiler,
  validatorCompiler,
} from '@marcalexiei/fastify-type-provider-zod';
import Fastify from 'fastify';
import { z } from 'zod';

const app = Fastify({ logger: true });
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);

app.register(fastifySwagger, {
  openapi: {
    info: {
      title: 'SampleApi',
      description: 'Sample backend service',
      version: '1.0.0',
    },
    servers: [],
  },
  transform: jsonSchemaTransform,
});

app.register(fastifySwaggerUI, {
  routePrefix: '/documentation',
});

const LOGIN_SCHEMA = z.object({
  username: z.string().max(32).describe('Some description for username'),
  password: z.string().max(32),
});

app.after(() => {
  app.withTypeProvider<ZodTypeProvider>().route({
    method: 'POST',
    url: '/login',
    schema: { body: LOGIN_SCHEMA },
    handler: (_req, res) => {
      res.send('ok');
    },
  });
});

await app.ready();

const url = await app.listen({ port: 4949 });

app.log.info(`Documentation running at ${url}`);

Customizing error responses

You can customize how Fastify handles request and response validation errors by extending your global error handler. Here's how to do it:

ts
import {
  hasZodFastifySchemaValidationErrors,
  isResponseSerializationError,
} from '@marcalexiei/fastify-type-provider-zod';
import Fastify from 'fastify';

const app = Fastify();

app.setErrorHandler((err, req, reply) => {
  if (hasZodFastifySchemaValidationErrors(err)) {
    return reply.code(400).send({
      error: 'Response Validation Error',
      message: "Request doesn't match the schema",
      statusCode: 400,
      details: {
        issues: err.validation,
        method: req.method,
        url: req.url,
      },
    });
  }

  if (isResponseSerializationError(err)) {
    return reply.code(500).send({
      error: 'Internal Server Error',
      message: "Response doesn't match the schema",
      statusCode: 500,
      details: {
        issues: err.cause.issues,
        method: err.method,
        url: err.url,
      },
    });
  }

  // the rest of the error handler
  return;
});

How to create refs to the schemas?

With zod globalRegistry

This plugin automatically generates JSON Schema references via the jsonSchemaTransformObject function. To enable this, you register your schemas in the global Zod registry and assign each one a unique id. Once registered, fastifySwagger will generate an OpenAPI document that references these schemas appropriately.

For example, the following code creates a reference to the User schema, ensuring it is included and properly linked in the OpenAPI specification.

ts
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import type { ZodTypeProvider } from '@marcalexiei/fastify-type-provider-zod';
import {
  jsonSchemaTransform,
  jsonSchemaTransformObject,
  serializerCompiler,
  validatorCompiler,
} from '@marcalexiei/fastify-type-provider-zod';
import Fastify from 'fastify';
import { z } from 'zod';

const USER_SCHEMA = z
  .object({
    id: z.number().int().positive(),
    name: z.string().describe('The name of the user'),
  })
  .meta({
    id: 'User', // <--- THIS MUST BE UNIQUE AMONG SCHEMAS
    description: 'User information',
  });

const app = Fastify({ logger: true });
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);

app.register(fastifySwagger, {
  openapi: {
    info: {
      title: 'SampleApi',
      description: 'Sample backend service',
      version: '1.0.0',
    },
    servers: [],
  },
  transform: jsonSchemaTransform,
  transformObject: jsonSchemaTransformObject,
});

app.register(fastifySwaggerUI, {
  routePrefix: '/documentation',
});

app.after(() => {
  app.withTypeProvider<ZodTypeProvider>().route({
    method: 'GET',
    url: '/users',
    schema: {
      response: {
        200: USER_SCHEMA.array(),
      },
    },
    handler: (_req, res) => {
      res.send([]);
    },
  });
});

await app.ready();

const url = await app.listen({ port: 4949 });

app.log.info(`Documentation running at ${url}`);

With custom schema registry

DANGER

If a custom registry is used, any properties added via .meta() are lost when .register() is called.

To prevent data loss, provide all metadata at the time of schema registration.

TIP

ZodOpenApiSchemaMetadata type can be used to have proper intelli

ts
import { z } from 'zod/v4';
import type { ZodOpenApiSchemaMetadata } from '@marcalexiei/fastify-type-provider-zod';

const customRegistry = z.registry<ZodOpenApiSchemaMetadata>();

z
  .string()
  .optional()
  .default('U1')
  .register(customRegistry{ 
    id: 'UserId', 
    description: 'User identifier',
    example: 'U234', 
  })

Here is the complete snippet:

ts
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import type {
  ZodOpenApiSchemaMetadata,
  ZodTypeProvider,
} from '@marcalexiei/fastify-type-provider-zod';
import {
  createJsonSchemaTransform,
  createJsonSchemaTransformObject,
  serializerCompiler,
  validatorCompiler,
} from '@marcalexiei/fastify-type-provider-zod';
import Fastify from 'fastify';
import { registry, z } from 'zod';

const schemaRegistry = registry<ZodOpenApiSchemaMetadata>();

const USER_SCHEMA = z
  .object({
    id: z.number().int().positive(),
    name: z.string().describe('The name of the user'),
  })
  .register(schemaRegistry, {
    id: 'User', // <--- THIS MUST BE UNIQUE AMONG SCHEMAS
    description: 'User information',
  });

const app = Fastify({ logger: true });
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);

app.register(fastifySwagger, {
  openapi: {
    info: {
      title: 'SampleApi',
      description: 'Sample backend service',
      version: '1.0.0',
    },
    servers: [],
  },
  transform: createJsonSchemaTransform({ schemaRegistry }),
  transformObject: createJsonSchemaTransformObject({ schemaRegistry }),
});

app.register(fastifySwaggerUI, {
  routePrefix: '/documentation',
});

app.after(() => {
  app.withTypeProvider<ZodTypeProvider>().route({
    method: 'GET',
    url: '/users',
    schema: {
      response: {
        200: USER_SCHEMA.array(),
      },
    },
    handler: (_req, res) => {
      res.send([]);
    },
  });
});

await app.ready();

const url = await app.listen({ port: 4949 });

app.log.info(`Documentation running at ${url}`);

How to create a plugin?

The plugin provides types that simplify writing both async and callback-style Fastify plugins, with full type inference based on your Zod schemas. This ensures request and response types are automatically inferred, reducing boilerplate and improving type safety across your routes.

ts
import type { FastifyPluginAsyncZod } from '@marcalexiei/fastify-type-provider-zod';
import { z } from 'zod';

export const plugin: FastifyPluginAsyncZod = async (fastify, _opts) => {
  fastify.route({
    method: 'GET',
    url: '/',
    // Define your schema
    schema: {
      querystring: z.object({
        name: z.string().min(4),
      }),
      response: {
        200: z.string(),
      },
    },
    handler: (req, res) => {
      res.send(req.query.name);
    },
  });

  await Promise.resolve();
};