Docs
Generating OpenAPI from Code
Fastify

OpenAPI and Fastify with Optic

To begin using Optic with your API you first need an OpenAPI file describing your API. This YAML or JSON file can be written by hand or generated from your code. This document describes the recommended process for generating a new OpenAPI file for a Fastify project.

Tools

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:

app.ts
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",
      },
    },
  });
 
  setupRoutes(app);
 
  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.

routes/create_user.ts
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) => {
  app.post<{
    Body: UserRequest;
    Reply: UserResponse;
  }>(
    "/api/users",
    {
      schema: {
        body: UserRequest,
        response: {
          200: UserResponse,
        },
      },
    },
    (request, reply) => {
      reply.code(200).send({
        id: uuidv4(),
        name: request.body.name,
      });
    }
  );
};
ℹ️

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 app.post<{ 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).

app.post<{
  Params: ApiPathParameters;
  Querystring: ApiQuerySchema;
  Body: ApiRequestSchema;
  Reply: ApiResponseSchema;
}>(
  "/api/path",
  {
    schema: {
      params: ApiPathParameters,
      body: ApiRequestSchema,
      querystring: ApiQuerySchema,
      response: {
        200: ApiResponseSchema,
      },
    },
  },
  (request, reply) => {
    request.query;
    request.params;
    request.body;
  }
);

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.

generate-spec.ts
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

Get a coverage report

You can measure how much of your OpenAPI spec is covered by your tests and find out about schema / OpenAPI discrepancies using the fastify-capture package.

Setup

  1. Add the package to your project:
yarn add @usoptic/fastify-capture
npm install @useoptic/fastify-capture
  1. Capture traffic during tests:
import { fastifyCapture } form '@useoptic/fastify-capture'

if (env === 'test') {
  app.addHook('onSend', fastifyCapture({
    harOutputDir: 'har-capture'
  }));
}

Usage

  1. Run your tests:
yarn run tests
npm run tests

If some of your tests hit the server with http requests then a har-capture folder was created, containing .har files.

  1. pass the generated .har archives to optic verify to measure coverage and check for discrepancies between your test network calls and your OpenAPI file:
optic verify ./openapi-spec.yaml --har ./har-capture

What's next

Automate your OpenAPI generation and test your API specifications by setting up Optic in CI.