@honorest/honorest-contract
v0.1.2
Published
Contract integration for HonorestJS - Enables contract-first API development with automatic validation
Maintainers
Readme
@honorest/honorest-contract
Contract integration for HonorestJS - Enables contract-first API development with automatic validation
@honorest/honorest-contract bridges @honorest/contract with the HonorestJS framework, enabling contract-first API development with automatic runtime validation.
Features
✨ Seamless Integration - Works directly with HonorestJS's decorator system
🔒 Automatic Validation - Validates requests and responses using contract schemas
✅ @Contract() Decorator - Simple decorator to apply contracts to methods
🎯 Type-Safe - Full TypeScript type inference from @honorest/contract
🚀 Zero Boilerplate - Just add the decorator, validation works automatically
⚡ Built-in Interceptor - Leverages HonorestJS's interceptor system
Installation
npm install @honorest/honorest-contract @honorest/contract honorestjs hono zod reflect-metadata
# or
yarn add @honorest/honorest-contract @honorest/contract honorestjs hono zod reflect-metadata
# or
pnpm add @honorest/honorest-contract @honorest/contract honorestjs hono zod reflect-metadata
# or
bun add @honorest/honorest-contract @honorest/contract honorestjs hono zod reflect-metadataQuick Start
1. Define Your Contract
First, create your API contract using @honorest/contract:
// contracts/users.contract.ts
import { defineContract, endpoint } from '@honorest/contract'
import { z } from 'zod'
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
})
export const UsersContract = defineContract({
name: 'users',
path: '/users',
endpoints: {
getUser: endpoint({
method: 'GET',
path: '/:id',
params: z.object({ id: z.string().uuid() }),
output: UserSchema,
errors: {
404: z.object({ message: z.string() })
}
}),
createUser: endpoint({
method: 'POST',
path: '/',
body: z.object({
name: z.string().min(1),
email: z.string().email()
}),
output: UserSchema
})
}
})2. Use @Contract() Decorator
Apply the @Contract() decorator to your controller methods. The decorator automatically registers the HTTP method and path from the contract, so you don't need separate HTTP method decorators:
// users.controller.ts
import 'reflect-metadata'
import { Controller, Param, Body } from 'honorestjs'
import { Contract } from '@honorest/honorest-contract'
import { UsersContract } from './contracts/users.contract'
import { UserService } from './user.service'
@Controller()
export class UsersController {
constructor(private readonly userService: UserService) {}
@Contract(UsersContract.endpoints.getUser)
async getUser(@Param('id') id: string) {
// Request validation happens automatically
const user = await this.userService.findById(id)
// Response validation happens automatically
return user
}
@Contract(UsersContract.endpoints.createUser)
async createUser(@Body() data: any) {
// Request body is automatically validated
const user = await this.userService.create(data)
// Response is automatically validated
return user
}
}That's it! The @Contract() decorator will automatically:
- ✅ Register the HTTP route (method + path) from the contract
- ✅ Validate request params, query, body, and headers
- ✅ Validate response data before sending to client
- ✅ Throw proper HTTP exceptions on validation failures
- ✅ Log validation errors for debugging
API Reference
@Contract() Decorator
Applies a contract endpoint to a controller method, enabling automatic validation and route registration.
@Contract(endpoint: Endpoint)Parameters:
endpoint- An endpoint definition from@honorest/contract
Key Points:
- The decorator extracts the HTTP method and path from the contract endpoint
- Do not use
@Get(),@Post(), etc. alongside@Contract() - The decorator automatically applies the
ContractInterceptorfor validation - Must be used with
import 'reflect-metadata'at the top of your file
Example:
@Contract(UsersContract.endpoints.getUser)
async getUser(@Param('id') id: string) {
return this.userService.findById(id)
}ContractInterceptor
The interceptor that performs the actual validation. This is automatically applied by the @Contract() decorator.
import { ContractInterceptor } from '@honorest/honorest-contract'Note: In most cases, you don't need to use this directly. The @Contract() decorator automatically applies it. This export is provided for advanced use cases where you need to apply contract validation to methods decorated with standard HTTP decorators.
ContractMetadataRegistry
Utility class for managing contract metadata (advanced usage).
// Check if a method has a contract
const hasContract = ContractMetadataRegistry.hasContract(
controllerClass,
methodName
)
// Get contract for a method
const contract = ContractMetadataRegistry.getContract(
controllerClass,
methodName
)How It Works
The plugin uses a simple but powerful architecture:
@Contract() Decorator - At application bootstrap:
- Extracts the HTTP method and path from the contract endpoint definition
- Registers the route with HonorestJS using
createHttpHandlerDecorator - Stores contract metadata using the Reflector service for type-safe access
- Automatically applies the
ContractInterceptorto the method
ContractInterceptor - During request/response lifecycle:
- BEFORE handler: Extracts request data (params, query, body, headers) and validates against the contract's input schemas using
validateEndpointInput - AFTER handler: Validates the handler's return value against the contract's output schema using
validateEndpointOutput - Throws
HTTPExceptionwith appropriate status codes on validation failures - Logs validation errors to console for debugging
- BEFORE handler: Extracts request data (params, query, body, headers) and validates against the contract's input schemas using
Metadata Management:
- Uses HonorestJS's
Reflectorservice for type-safe metadata storage - Stores contract endpoints on controller method prototypes
- Manages interceptor metadata to avoid duplicates
- Uses HonorestJS's
Validation Behavior
Request Validation (Input)
- ✅ Validates
params,query,body, andheadersif schemas are defined in the contract - ✅ Throws
HTTPException 400with error details on validation failure - ✅ Only validates fields that have schemas defined
Response Validation (Output)
- ✅ Validates the handler's return value against the
outputschema - ✅ Throws
HTTPException 500on validation failure (prevents sending invalid data to client) - ✅ Logs validation errors to console for debugging
Error Handling
Request Validation Failure (400 Bad Request):
{
"message": "Request validation failed",
"cause": [
{ "field": "params", "message": "Invalid path parameters", "code": "INVALID_PARAMS" }
]
}Response Validation Failure (500 Internal Server Error):
{
"message": "Internal server error: Invalid response format"
}Invalid Request Format (400 Bad Request):
{
"message": "Invalid request format",
"cause": "Error details"
}Best Practices
1. Share Contracts Across Projects
Keep your contracts in a separate package that both server and client can import:
/packages
/api-contract # Shared contracts
/src
/users.contract.ts
/posts.contract.ts
index.ts
/api-server # HonorestJS server
/web-client # Frontend application2. Always Define Output Schemas
Even if you trust your implementation, always define output schemas to catch bugs early:
// ❌ Bad - No output validation
output: z.any()
// ✅ Good - Explicit validation
output: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
})3. Use Error Schemas for Expected Errors
Define schemas for error responses to maintain type safety:
endpoint({
// ...
errors: {
400: z.object({ message: z.string(), errors: z.array(z.any()) }),
404: z.object({ message: z.string() }),
500: z.object({ message: z.string() })
}
})Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © HonorestJS
Related Packages
honorestjs- HonorestJS server framework@honorest/contract- Framework-agnostic contract definitions@honorest/client- Type-safe HTTP client SDK
