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
- 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
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
- Add the package to your project:
yarn add @usoptic/fastify-capture
npm install @useoptic/fastify-capture
- Capture traffic during tests:
import { fastifyCapture } form '@useoptic/fastify-capture'
if (env === 'test') {
app.addHook('onSend', fastifyCapture({
harOutputDir: 'har-capture'
}));
}
Usage
- 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.
- pass the generated
.har
archives tooptic 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.