UNPKG

@honorestjs/contract

Version:

Contract-first API definitions for HonorestJS - Framework-agnostic contract definitions with full type safety

388 lines (305 loc) • 9.6 kB
# @honorestjs/contract > Framework-agnostic contract definitions for type-safe APIs [![npm version](https://img.shields.io/npm/v/@honorestjs/contract.svg)](https://www.npmjs.com/package/@honorestjs/contract) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) `@honorestjs/contract` provides a powerful, type-safe way to define API contracts that can be shared between servers and clients. Built on top of [Zod](https://github.com/colinhacks/zod), it enables **contract-first development** with full TypeScript type inference and runtime validation. ## Features ✨ **Contract-First Development** - Define your API before implementation šŸ”’ **Full Type Safety** - Complete TypeScript type inference from contracts āœ… **Runtime Validation** - Built-in validation using Zod schemas šŸŽÆ **Framework Agnostic** - Use with any server or client framework šŸ“ **OpenAPI Compatible** - Generate OpenAPI specs from contracts šŸš€ **Zero Code Generation** - Pure TypeScript, no build steps required ## Installation ```bash npm install @honorestjs/contract zod # or yarn add @honorestjs/contract zod # or pnpm add @honorestjs/contract zod # or bun add @honorestjs/contract zod ``` ## Quick Start ### 1. Define Your Contract ```typescript import { defineContract, endpoint } from '@honorestjs/contract' import { z } from 'zod' // Define schemas const UserSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email() }) const CreateUserSchema = z.object({ name: z.string().min(1), email: z.string().email() }) // Define contract export const UsersContract = defineContract({ name: 'users', path: '/users', description: 'User management endpoints', endpoints: { getUser: endpoint({ method: 'GET', path: '/:id', params: z.object({ id: z.string().uuid() }), output: UserSchema, description: 'Get a user by ID', errors: { 404: z.object({ message: z.string() }) } }), listUsers: endpoint({ method: 'GET', path: '/', query: z.object({ page: z.number().int().positive().default(1), limit: z.number().int().positive().max(100).default(20) }), output: z.object({ users: z.array(UserSchema), total: z.number() }) }), createUser: endpoint({ method: 'POST', path: '/', body: CreateUserSchema, output: UserSchema, errors: { 400: z.object({ message: z.string(), errors: z.array(z.any()) }) } }) } }) ``` ### 2. Create an App Contract ```typescript import { defineApp } from '@honorestjs/contract' import { UsersContract } from './contracts/users' import { PostsContract } from './contracts/posts' export const AppContract = defineApp({ version: 1, prefix: '/api', contracts: { users: UsersContract, posts: PostsContract }, metadata: { title: 'My API', version: '1.0.0', description: 'RESTful API for my application' } }) ``` ### 3. Use Type Inference ```typescript import type { ContractInput, ContractOutput } from '@honorestjs/contract' // Extract input types type GetUserInput = ContractInput<typeof UsersContract, 'getUser'> // { params: { id: string }, query: never, body: never, headers: never } // Extract output types type GetUserOutput = ContractOutput<typeof UsersContract, 'getUser'> // { id: string, name: string, email: string } // Use in your implementation function getUser(input: GetUserInput['params']): Promise<GetUserOutput> { // Implementation } ``` ## API Reference ### Builders #### `endpoint(definition)` Creates an endpoint definition. ```typescript const getUserEndpoint = endpoint({ method: 'GET', // HTTP method path: '/:id', // Endpoint path params: z.object({ id: z.string() }), // Path parameters schema query: z.object({ include: z.string() }), // Query parameters schema (optional) body: z.object({ ... }), // Request body schema (optional) headers: z.object({ ... }), // Headers schema (optional) output: z.object({ ... }), // Response schema errors: { // Error responses (optional) 404: z.object({ message: z.string() }) }, description: 'Get a user by ID', // Description (optional) summary: 'Get user', // Summary (optional) tags: ['users'], // Tags (optional) deprecated: false // Deprecated flag (optional) }) ``` #### `defineContract(definition)` Creates a contract (collection of endpoints). ```typescript const UsersContract = defineContract({ name: 'users', // Contract name path: '/users', // Base path version: 1, // API version (optional) description: 'User endpoints', // Description (optional) tags: ['users'], // Tags (optional) endpoints: { // Endpoint definitions getUser: getUserEndpoint, createUser: createUserEndpoint } }) ``` #### `defineApp(definition)` Creates an app (collection of contracts). ```typescript const AppContract = defineApp({ version: 1, // Global API version (optional) prefix: '/api', // Global prefix (optional) contracts: { // Contract definitions users: UsersContract, posts: PostsContract }, metadata: { // App metadata (optional) title: 'My API', version: '1.0.0', description: 'API description', contact: { name: 'Support', email: 'support@example.com' }, license: { name: 'MIT' } } }) ``` ### Type Utilities #### `ContractInput<TContract, TEndpoint>` Extracts input type from a contract endpoint. ```typescript type Input = ContractInput<typeof UsersContract, 'getUser'> // { params: {...}, query: {...}, body: {...}, headers: {...} } ``` #### `ContractOutput<TContract, TEndpoint>` Extracts output type from a contract endpoint. ```typescript type Output = ContractOutput<typeof UsersContract, 'getUser'> // { id: string, name: string, email: string } ``` #### `ContractErrors<TContract, TEndpoint>` Extracts error types from a contract endpoint. ```typescript type Errors = ContractErrors<typeof UsersContract, 'getUser'> // { 404: { message: string } } ``` ### Validation #### `validate(schema, data)` Validates data against a Zod schema (async). ```typescript const result = await validate(UserSchema, userData) if (result.success) { console.log(result.data) // Validated data } else { console.error(result.error) // Validation error } ``` #### `validateEndpointInput(endpoint, input)` Validates all inputs for an endpoint. ```typescript const validation = await validateEndpointInput( UsersContract.endpoints.getUser, { params: { id: '123' }, query: { include: 'posts' } } ) if (!validation.isValid) { console.error(validation.errors) } ``` #### `validateEndpointOutput(endpoint, output)` Validates endpoint output. ```typescript const result = await validateEndpointOutput( UsersContract.endpoints.getUser, responseData ) ``` ## Integration ### With HonorestJS Server ```typescript import { Controller } from 'honorestjs' import { Contract } from 'honorestjs/contract' import { UsersContract } from '@myapp/api-contract' @Controller() export class UsersController { @Contract(UsersContract.endpoints.getUser) async getUser(@Param('id') id: string) { // Implementation with automatic validation } } ``` ### With @honorest/client ```typescript import { createClient } from '@honorest/client' import { AppContract } from '@myapp/api-contract' const api = createClient(AppContract, { baseUrl: 'http://localhost:3000' }) // Fully type-safe const user = await api.users.getUser({ params: { id: '123' }}) ``` ## Best Practices ### 1. Organize Contracts by Domain ``` /contracts /users schemas.ts contract.ts /posts schemas.ts contract.ts index.ts # Export AppContract ``` ### 2. Reuse Schemas ```typescript // Define shared schemas export const UserSchema = z.object({ ... }) export const CreateUserSchema = UserSchema.omit({ id: true }) export const UpdateUserSchema = CreateUserSchema.partial() ``` ### 3. Document Everything ```typescript const endpoint = endpoint({ // ... description: 'Detailed description of what this endpoint does', summary: 'Short summary', tags: ['users', 'admin'], examples: { request: { id: '123' }, response: { id: '123', name: 'John' } } }) ``` ### 4. Version Your Contracts ```typescript const UsersContractV1 = defineContract({ name: 'users', version: 1, // ... }) const UsersContractV2 = defineContract({ name: 'users', version: 2, // ... }) ``` ## TypeScript Configuration For best results, enable strict mode in your `tsconfig.json`: ```json { "compilerOptions": { "strict": true, "strictNullChecks": true } } ``` ## Contributing Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details. ## License MIT Ā© [HonorestJS](https://github.com/honorestjs) ## Related Packages - [`honorestjs`](https://github.com/honorestjs/honorest) - Server framework - [`@honorest/client`](https://github.com/honorestjs/client) - Type-safe client SDK - [`@honorest/openapi`](https://github.com/honorestjs/openapi) - OpenAPI generation (coming soon)