Package Exports
- @honorestjs/contract
Readme
@honorestjs/contract
Framework-agnostic contract definitions for type-safe APIs
@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, 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
npm install @honorestjs/contract zod
# or
yarn add @honorestjs/contract zod
# or
pnpm add @honorestjs/contract zod
# or
bun add @honorestjs/contract zodQuick Start
1. Define Your Contract
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
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
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.
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).
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).
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.
type Input = ContractInput<typeof UsersContract, 'getUser'>
// { params: {...}, query: {...}, body: {...}, headers: {...} }ContractOutput<TContract, TEndpoint>
Extracts output type from a contract endpoint.
type Output = ContractOutput<typeof UsersContract, 'getUser'>
// { id: string, name: string, email: string }ContractErrors<TContract, TEndpoint>
Extracts error types from a contract endpoint.
type Errors = ContractErrors<typeof UsersContract, 'getUser'>
// { 404: { message: string } }Validation
validate(schema, data)
Validates data against a Zod schema (async).
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.
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.
const result = await validateEndpointOutput(
UsersContract.endpoints.getUser,
responseData
)Integration
With HonorestJS Server
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
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 AppContract2. Reuse Schemas
// Define shared schemas
export const UserSchema = z.object({ ... })
export const CreateUserSchema = UserSchema.omit({ id: true })
export const UpdateUserSchema = CreateUserSchema.partial()3. Document Everything
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
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:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true
}
}Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © HonorestJS
Related Packages
honorestjs- Server framework@honorest/client- Type-safe client SDK@honorest/openapi- OpenAPI generation (coming soon)