@origins-digital/ts-rest-zod-openapi
v1.2.0
Published
Origins Ts Rest Zod OpenAPI
Readme
@origins-digital/ts-rest-zod-openapi
A custom transformer for @ts-rest/openapi that uses @origins-digital/zod-openapi-utils instead of @anatine/zod-openapi and is able to deal with lazy schemes.
Installation
npm install @origins-digital/ts-rest-zod-openapiUsage
Basic Setup
Replace the default @anatine/zod-openapi transformer with our custom one:
import { generateOpenApi } from '@origins-digital/ts-rest-zod-openapi';
import { initServer } from '@ts-rest/core';
import { z } from 'zod';
// Create your server
const server = initServer();
// Define your contracts
const userContract = server.contract({
method: 'GET',
path: '/users',
responses: {
200: z.object({
users: z.array(
z.object({
id: z.string().describe('User ID'),
name: z.string().describe('User name'),
email: z.string().email().describe('User email'),
}),
),
}),
},
});
// Generate OpenAPI with custom transformer
const openApiDoc = generateOpenApi([userContract], {
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation',
},
});With NestJS
import { NestExpressApplication } from '@nestjs/platform-express';
import { SwaggerModule } from '@nestjs/swagger';
import { generateOpenApi } from '@origins-digital/ts-rest-zod-openapi';
import { specContract } from './contracts';
export function setupSwagger(app: NestExpressApplication, env: string) {
if (env === 'development' || env === 'local') {
try {
const openApiDocument = generateOpenApiW(
specContract.orchestratorOutput,
{
info: {
title: 'ACM Adapter API',
version: '1.0.0',
description:
'This is the ACM Orchestrator API OpenAPI specification.',
},
components: {
securitySchemes: {
bearer: {
scheme: 'bearer',
bearerFormat: 'JWT',
type: 'http',
},
},
},
},
);
SwaggerModule.setup('docs', app, openApiDocument);
} catch (error) {
console.warn('Failed to generate OpenAPI documentation:', error);
}
}
}With Express
import express from 'express';
import { generateOpenApi } from '@origins-digital/ts-rest-zod-openapi';
const app = express();
// Generate OpenAPI documentation
const openApiDoc = generateOpenApi(contracts, {
info: {
title: 'Express API',
version: '1.0.0',
},
});
// Serve OpenAPI documentation
app.get('/api-docs', (req, res) => {
res.json(openApiDoc);
});API Reference
generateOpenApiWithCustomTransformer(contracts, options)
Generates OpenAPI documentation using the custom transformer.
Parameters
contracts: Array of ts-rest contractsoptions: OpenAPI configuration optionsinfo: API information (title, version, description)servers: Array of server configurationscomponents: OpenAPI componentssecurity: Security schemestags: API tags
Returns
OpenAPI specification object compatible with Swagger UI.
Benefits
- Custom Transformation: Uses
@origins-digital/zod-openapi-utilsinstead of@anatine/zod-openapi - Consistent Schema: Same transformation logic across your entire application
- Better Control: Full control over how Zod schemas are converted to OpenAPI
- No External Dependencies: Removes dependency on
@anatine/zod-openapi
Migration from @anatine/zod-openapi
Replace:
import { generateOpenApi } from '@ts-rest/openapi';
const openApiDoc = generateOpenApi(contracts, options);With:
import { generateOpenApi } from '@origins-digital/ts-rest-zod-openapi';
const openApiDoc = generateOpenApi(contracts, options);Examples
Complex Schema Example with Lazy Schemas
import { z } from 'zod';
// Base schemas
const externalLinkSchema = z
.object({
type: z.literal('external_link'),
linkName: z.string().optional(),
linkURL: z.string().optional(),
isVisible: z.boolean(),
})
.openapi({
title: 'ExternalLink',
description: 'External link configuration',
});
const menuItemSchema = z
.object({
type: z.literal('menu_item_web'),
label: z.string(),
geoTarget: z
.object({
enable: z.boolean(),
type: z.enum(['allow', 'deny']),
countries: z.array(z.string()),
})
.optional(),
webPagesCodenamesAndSlugs: z.array(
z.object({
codename: z.string(),
slug: z.string(),
}),
),
isVisible: z.boolean(),
})
.openapi({
title: 'MenuItem',
description: 'Menu item configuration',
});
// Recursive schema with lazy loading
const menuItemWithSubItemsSchema = menuItemSchema
.extend({
subItems: z
.array(
z.union([z.lazy(() => menuItemWithSubItemsSchema), externalLinkSchema]),
)
.optional(),
})
.openapi({
title: 'MenuItemWithSubItems',
description: 'Menu item with nested sub-items',
});
// Main contract
const complexContract = server.contract({
method: 'GET',
path: '/api/web-config',
headers: z.object({
'Accept-Language': z.string().optional(),
'x-account-key': z.string(),
}),
responses: {
200: z
.object({
type: z.literal('web_config'),
menu: z.object({
menuItems: z.array(
z.union([menuItemWithSubItemsSchema, externalLinkSchema]),
),
}),
footer: z.object({
copyright: z.string(),
legalLinks: z.array(
z.union([menuItemWithSubItemsSchema, externalLinkSchema]),
),
}),
errorMessage: z.object({
error404: z.string(),
genericError: z.string(),
}),
})
.openapi({
title: 'WebConfig',
description: 'Web configuration with menu and footer',
}),
},
});Contract Structure Example
import { initContract } from '@ts-rest/core';
const contract = initContract();
const webConfigEndpoints = {
getWebConfig: {
method: 'GET',
path: '/api/web-config',
headers: z.object({
'Accept-Language': z.string().optional(),
'x-account-key': z.string(),
}),
responses: {
200: webConfigSchema,
},
summary: 'Get web config',
metadata: {
openApiSecurity: [
{
bearer: [],
},
],
},
},
} as const;
export const webConfigContract = contract.router(webConfigEndpoints);
const mainContract = initContract();
export const specContract = mainContract.router({
orchestratorOutput: {
webconfig: webConfigContract,
},
});License
MIT
