@honorestjs/contract
Version:
Contract-first API definitions for HonorestJS - Framework-agnostic contract definitions with full type safety
388 lines (305 loc) ⢠9.6 kB
Markdown
# @honorestjs/contract
> Framework-agnostic contract definitions for type-safe APIs
[](https://www.npmjs.com/package/@honorestjs/contract)
[](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)