OpenAPI and Fastify with Optic

Connecting your API to Optic is basically finding a way to get an OpenAPI file that's representative of your API. If you already have an OpenAPI file that you write by hand or generate yourself, you can continue to our CI setup guides (GitHub or GitLab).

In this guide, we'll learn how to configure your Fastify app (opens in a new tab) to start generating an OpenAPI file.


npm i fastify @fastify/swagger @sinclair/typebox

Configure your app

Firstly, we need to configure out app to generate an OpenAPI file. In your app setup:

import Fastify, { FastifyInstance } from "fastify";
import fastifySwagger from "@fastify/swagger";
export const setupApp = async () => {
  const app = Fastify();
  await app.register(fastifySwagger, {
    // Opt into OpenAPIV3 generation - Optic supports OpenAPI 3 and 3.1
    openapi: {
      openapi: "3.1.3",
      info: {
        title: "My api spec",
        version: "1.0.0",
  return app;
const setupRoutes = (app: FastifyInstance) => {
  // ... set up route definitions

If you are familiar with OpenAPI, you'll notice that the openapi key maps directly to the root of an OpenAPI document. Meaning that if you have custom extensions you can specify them here (like x-optic-standard or x-optic-url)!

Configure your route definitions

Now that we've opted into OpenAPI generation, it's time to define the request / response schemas for our endpoints. In this example, we'll create a POST /api/users endpoint.

import Fastify, { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
const UserRequest = Type.Object({ name: Type.String() });
type UserRequest = Static<typeof UserRequest>;
const UserResponse = Type.Object({ id: Type.String(), name: Type.String() });
type UserResponse = Static<typeof UserResponse>;
const registerCreateUser = (app: FastifyInstance) => {<{
    Body: UserRequest;
    Reply: UserResponse;
      schema: {
        body: UserRequest,
        response: {
          200: UserResponse,
    (request, reply) => {
        id: uuidv4(),

Why do we need two UserRequest and UserResponse variables?

The const UserRequest is the JSON schema representation, and the type UserRequest is the typescript type definition.

You'll notice that they're used both as types (in the<{ Body: UserRequest }> and in the schema schema: { body: UserRequest }). This is so that we get:

  • Request + response validation
  • Typescript static type checking
  • OpenAPI schema creation

You can view the documentation on Typebox (opens in a new tab) to see how to define other types and you can also add request / response schemas for query parameters and path parameters in fastify (example below).<{
  Params: ApiPathParameters;
  Querystring: ApiQuerySchema;
  Body: ApiRequestSchema;
  Reply: ApiResponseSchema;
    schema: {
      params: ApiPathParameters,
      body: ApiRequestSchema,
      querystring: ApiQuerySchema,
      response: {
        200: ApiResponseSchema,
  (request, reply) => {

Generate your OpenAPI spec

Now that we've defined our routes and configured our app, all that's left is to generate our OpenAPI spec. To do this, we'll need to create a separate file to be run whenever we need to generate a spec.

import yaml from "yaml";
import fs from "node:fs/promises";
import { setupApp } from "./app";
const FILE_PATH = "./openapi-spec.yaml";
(async () => {
  const app = await setupApp();
  await app.ready();
  const yamlContents = yaml.stringify(app.swagger());
  await fs.writeFile(FILE_PATH, yamlContents);
  console.log(`Successfully created OpenAPI spec at ${FILE_PATH}`)

Great - all that's left to do is to run this!

npx ts-node ./generate-spec.ts

> Successfully created OpenAPI spec at ./openapi-spec.yaml

What's next

Now that we've got a way to generate a spec from our code definition, we can set up Optic in CI to check for breaking changes, always keeping documentation in sync with the API and keep our OpenAPI accurate. Follow our CI setup guides (GitHub or GitLab).