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.
Tools
- fastify (opens in a new tab)
- don't have a fastify app? take a look at our fastify integration example (opens in a new tab) here to kick start your app
- fastify-swagger (opens in a new tab)
- typebox (opens in a new tab) for JSON schema definitions and typescript types
- yaml (opens in a new tab) - creates YAML strings
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",
},
},
});
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.
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.
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).