Open API
Installation
Much like the other packages, @ts-rest/open-api
is easy to download. If you also want to serve a Swagger UI, you'll want to download swagger-ui-express
for express or @nestjs/swagger
for Nest.
- pnpm
- npm
- yarn
pnpm add @ts-rest/open-api
npm install @ts-rest/open-api
yarn add @ts-rest/open-api
Generating an OpenAPI Document
import { myContract } from './my-api';
import { generateOpenApi } from '@ts-rest/open-api';
const openApiDocument = generateOpenApi(myContract, {
info: {
title: 'Posts API',
version: '1.0.0',
},
});
No kidding, that's it 🥳 You can now serve this document however you want, use it for CodeGen for your non-TS clients, or use it to generate a Swagger UI!
Extending Schemas with @anatine/zod-openapi
Since our OpenAPI generator uses @anatine/zod-openapi
, you can extend your Zod schemas with additional OpenAPI information like title
, description
, and example
to improve the quality and clarity of the generated documentation.
To do so, extend Zod with extendZodWithOpenApi(z)
when defining the schemas for your contract:
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
extendZodWithOpenApi(z);
const c = initContract();
export const contract = c.router({
getUser: {
method: 'GET',
path: '/users/:id',
pathParams: z.object({
id: z.string().openapi({
description: "The user's ID",
}),
}),
responses: {
200: z
.object({
id: z.string().uuid().openapi({
title: 'Unique ID',
description: 'A UUID generated by the server',
}),
name: z.string(),
phoneNumber: z.string().min(10).openapi({
description: 'US phone numbers only',
example: '555-555-5555',
}),
})
.openapi({
title: 'User',
description: 'A user schema',
mediaExamples: {
myExample: {
value: {
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
phoneNumber: '555-555-5555',
},
summary: 'Example of a user',
},
},
}),
},
},
});
See the official @anatine/zod-openapi
docs for more information.
All Zod schemas defined in your contract can benefit from the additional OpenAPI schema, including pathParams
, queryParams
, and responses
. These keys are all used when generating the OpenAPI JSON file. This could improve the quality of your generated documentation.
Adding Examples to the Media Type
In order to add examples to the media type rather than to the schema itself (this is useful if you want to show multiple examples), we have added a mediaExamples
property to the .openapi()
method options.
This will only work for the schemas of the body, responses and individual query parameters if you are using jsonQuery
option.
You can see an example of its usage in the code snippet above.
Extending Operations with Additional OpenAPI Fields
We do not provide first-party support to set all possible OpenAPI fields on the operations such as the security
field. In addition, you may have some specific needs to modify the fields already set by ts-rest
such as the tags
field.
Therefore, we have provided an operationMapper
option to allow you to modify the OpenAPI fields of the operations. This is a callback function, that will receive the operation object and the contract endpoint, and must return a valid OpenAPI operation object.
A common way to provide data to this function is to utilize the metadata
field of the contract endpoint. However, feel free to come up with a different solution to doing this if you would not like to include this data in your contracts.
const hasCustomTags = (
metadata: unknown,
): metadata is { openApiTags: string[] } => {
return (
!!metadata &&
typeof metadata === 'object' &&
'openApiTags' in metadata
);
};
const hasSecurity = (
metadata: unknown,
): metadata is { openApiSecurity: SecurityRequirementObject[] } => {
return (
!!metadata &&
typeof metadata === 'object' &&
'openApiSecurity' in metadata
);
};
const apiDoc = generateOpenApi(
router,
{
info: { title: 'Blog API', version: '0.1' },
components: {
securitySchemes: {
BasicAuth: {
type: 'http',
scheme: 'basic',
},
},
},
},
{
operationMapper: (operation, appRoute) => ({
...operation,
...(hasCustomTags(appRoute.metadata)
? {
tags: appRoute.metadata.openApiTags,
}
: {}),
...(hasSecurity(appRoute.metadata)
? {
security: appRoute.metadata.openApiSecurity,
}
: {}),
}),
},
);
Serving a Swagger UI
In Express use swagger-ui-express
:
import { myContract } from './my-api';
import { generateOpenApi } from '@ts-rest/open-api';
import * as swaggerUi from 'swagger-ui-express';
const openApiDocument = generateOpenApi(myContract, {
info: {
title: 'Posts API',
version: '1.0.0',
},
});
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiDocument));
In NestJS use @nestjs/swagger
:
import { myContract } from './my-api';
import { generateOpenApi } from '@ts-rest/open-api';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const document = generateOpenApi(myContract, {
info: {
title: 'Posts API',
version: '1.0.0',
},
});
SwaggerModule.setup('api-docs', app, document);
// ^ Path for swagger
await app.listen(3000);
}
bootstrap();
In Fastify use @fastify/swagger
and @fastify/swagger-ui
:
import { myContract } from './my-api';
import { generateOpenApi } from '@ts-rest/open-api';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
const openApiDocument = generateOpenApi(myContract, {
info: {
title: 'Posts API',
version: '1.0.0',
},
});
app.register(fastifySwagger, {
transformObject: () => openApiDocument
})
.register(fastifySwaggerUI, {
routePrefix: '/api-docs',
});
Don't worry if you don't use express or Nest, whatever library you want to use is OK our OpenAPI returns a plain JSON object which is fully compliant with the OpenAPI spec.
Enabling operationId
s (Recommended!)
You can set setOperationId
to either true
or concatenated-path
to set operationId
s on your endpoints.
In the case of setting it to true
, it will use only the endpoint name from your contract. You have to ensure that the endpoint names are unique across the entire contract.
In the case of setting it to concatenated-path
, it will use the endpoint name concatenated with the path through the nested contract.
This is useful when you have multiple endpoints with the same name but different paths. This will result in longer but more descriptive operationId
s.
const openApiSchema = generateOpenApi(
postsApi,
{
info: {
title: 'Posts API',
version: '1.0.0',
},
},
{
setOperationId: true,
// setOperationId: 'concatenated-path',
},
);
Below is an example of what the OpenAPI document would look like with operationId
's enabled:
{
"openapi": "3.0.2",
"paths": {
"/posts": {
"get": {
"description": "Get all posts",
"tags": [],
"parameters": [
{
"name": "userId",
"in": "query",
"schema": {
"type": "number"
}
}
],
"operationId": "getPosts", // <--- This is the operationId
// or
"operationId": "posts.getPosts", // <--- If using concatenated-path
"responses": {
JSON Query Params
If you've enabled JSON Query params for your server and client, you can enable jsonQuery
to mark the query params as application/json
in the OpenAPI document:
const openApiSchema = generateOpenApi(
postsApi,
{
info: {
title: 'Posts API',
version: '1.0.0',
},
},
{
jsonQuery: true,
},
);
You'll want to do this to let your non ts-rest clients know that they should send the query params as JSON.