npm package: @origins-digital/ts-rest-zod-openapi
Description: Origins Ts Rest Zod OpenAPI
Author: Origins Digital
Latest version: 2.1.4

# @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

```bash
npm install @origins-digital/ts-rest-zod-openapi
```

## Usage

### Basic Setup

Replace the default `@anatine/zod-openapi` transformer with our custom one:

```typescript
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

```typescript
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

```typescript
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 contracts
- `options`: OpenAPI configuration options
  - `info`: API information (title, version, description)
  - `servers`: Array of server configurations
  - `components`: OpenAPI components
  - `security`: Security schemes
  - `tags`: API tags

#### Returns

OpenAPI specification object compatible with Swagger UI.

## Benefits

- **Custom Transformation**: Uses `@origins-digital/zod-openapi-utils` instead 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:

```typescript
import { generateOpenApi } from '@ts-rest/openapi';

const openApiDoc = generateOpenApi(contracts, options);
```

With:

```typescript
import { generateOpenApi } from '@origins-digital/ts-rest-zod-openapi';

const openApiDoc = generateOpenApi(contracts, options);
```

## Examples

### Complex Schema Example with Lazy Schemas

```typescript
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(),
  })
  .meta({
    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(),
  })
  .meta({
    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(),
  })
  .meta({
    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(),
        }),
      })
      .meta({
        title: 'WebConfig',
        description: 'Web configuration with menu and footer',
      }),
  },
});
```

### Contract Structure Example

```typescript
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