@kontract/adonis
v0.4.0
Published
AdonisJS adapter for kontract
Maintainers
Readme
@kontract/adonis
AdonisJS adapter for kontract. Provides route registration, AJV validation, Lucid ORM serializers, and OpenAPI spec generation for AdonisJS v6.
Features
- Automatic route registration - Register decorated endpoints with AdonisJS router
- AJV validation - TypeBox schema validation with type coercion and format support
- Lucid serializers - Automatic serialization of Lucid models and paginators
- OpenAPI generation - Build OpenAPI 3.0/3.1 specs from decorators
- Full TypeBox support - Validate requests against TypeBox schemas
Installation
npm install @kontract/adonis @sinclair/typebox ajv ajv-formatsNote: This package depends on
kontract(the core library), which will be installed automatically. You can import from either package - the adapter re-exports all commonly used items from the core.
Quick Start
1. Define Your Controller
// app/controllers/books_controller.ts
import { Api, Endpoint, ok, apiError } from '@kontract/adonis'
import { Type, Static } from '@sinclair/typebox'
import Book from '#models/book'
const BookSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
title: Type.String(),
author: Type.String(),
}, { $id: 'Book' })
const CreateBookRequest = Type.Object({
title: Type.String({ minLength: 1 }),
author: Type.String({ minLength: 1 }),
}, { $id: 'CreateBookRequest' })
@Api({ tag: 'Books', description: 'Book management' })
export default class BooksController {
@Endpoint('GET /api/v1/books', {
summary: 'List all books',
responses: {
200: { schema: Type.Array(BookSchema) },
},
})
async index() {
const books = await Book.all()
return ok(Type.Array(BookSchema), books.map(b => b.toResponse()))
}
@Endpoint('POST /api/v1/books', {
summary: 'Create a book',
auth: 'required',
body: CreateBookRequest,
responses: {
201: { schema: BookSchema },
422: null,
},
})
async store(
ctx: HttpContext,
body: Static<typeof CreateBookRequest>
) {
const book = await Book.create(body)
return ok(BookSchema, book.toResponse())
}
}2. Register Routes
// start/routes.ts
import router from '@adonisjs/core/services/router'
import { registerDecoratorRoutes, validate } from '@kontract/adonis'
// Import controllers to trigger decorator registration
import '#controllers/books_controller'
import '#controllers/users_controller'
// Register all decorated routes
registerDecoratorRoutes(router, { validate })3. Generate OpenAPI Spec
// commands/generate_openapi.ts
import { OpenApiBuilder } from '@kontract/adonis'
// Import controllers
import '#controllers/books_controller'
import '#controllers/users_controller'
const builder = new OpenApiBuilder({
title: 'My API',
description: 'API documentation',
version: '1.0.0',
servers: [
{ url: 'http://localhost:3333', description: 'Development' },
],
})
const spec = builder.build()
console.log(JSON.stringify(spec, null, 2))Runtime Validation
The packages support runtime validation for both requests and responses.
Request Validation (Automatic)
Request validation happens automatically when you use registerDecoratorRoutes(). The route registrar validates body, query, and params against the TypeBox schemas defined in your @Endpoint decorators.
@Endpoint('POST /api/v1/books', {
body: CreateBookRequest, // Validated at runtime
query: PaginationQuery, // Validated at runtime
params: BookIdParams, // Validated at runtime
responses: { 201: BookSchema },
})
async store(ctx, body, query, params) {
// body, query, params are already validated and typed
}If validation fails, an AjvValidationError is thrown with status 422.
Response Validation (Optional)
Response validation catches contract violations during development. Enable it by calling defineConfig() at application startup:
// start/kernel.ts or providers/app_provider.ts
import { defineConfig } from 'kontract'
import { createAjvValidator } from '@kontract/adonis'
const validator = createAjvValidator()
defineConfig({
openapi: {
info: { title: 'My API', version: '1.0.0' },
},
runtime: {
validateResponses: process.env.NODE_ENV !== 'production',
},
validator: (schema, data) => validator.validate(schema, data),
})When enabled, response helpers like ok(), created(), etc. will validate the response data against the schema and throw ResponseValidationError if it doesn't match.
// This will throw in development if user doesn't match UserSchema
return ok(UserSchema, user)Route Registration
registerDecoratorRoutes(router, options)
Registers all routes defined via @Endpoint decorators with the AdonisJS router.
import router from '@adonisjs/core/services/router'
import { registerDecoratorRoutes, validate } from '@kontract/adonis'
registerDecoratorRoutes(router, {
validate, // AJV validation function
})The registrar:
- Creates routes for each
@Endpointdecorator - Validates request body, query, and params against TypeBox schemas
- Handles authentication based on
authoption - Calls the controller method with validated data
- Processes API responses (status codes, JSON, binary)
Controller Method Signature
Controller methods receive validated data as separate parameters:
async store(
ctx: HttpContext, // AdonisJS context
body: BodyType, // Validated request body
query: QueryType, // Validated query parameters
params: ParamsType // Validated path parameters
) {
// body, query, params are already validated
}Validation
validate(schema, data)
Validates data against a TypeBox schema. Throws AjvValidationError on failure.
import { validate } from '@kontract/adonis'
import { Type } from '@sinclair/typebox'
const schema = Type.Object({
email: Type.String({ format: 'email' }),
age: Type.Integer({ minimum: 0 }),
})
try {
const data = validate(schema, { email: '[email protected]', age: 25 })
// data is typed and validated
} catch (error) {
if (error instanceof AjvValidationError) {
console.log(error.errors)
// [{ field: 'email', message: 'must match format "email"' }]
}
}createAjvValidator(options?)
Create a customized AJV validator instance.
import { createAjvValidator } from '@kontract/adonis'
const validator = createAjvValidator({
coerceTypes: true, // Convert strings to numbers, etc.
removeAdditional: true, // Strip unknown properties
useDefaults: true, // Apply default values
formats: {
'custom-format': (value) => /^[A-Z]+$/.test(value),
},
ajvOptions: {
// Additional AJV options
},
})
// Validate (returns errors array)
const errors = validator.validate(schema, data)
// Validate or throw
validator.validateOrThrow(schema, data)
// Pre-compile for performance
const compiled = validator.compile(schema)
compiled.validate(data)
compiled.validateOrThrow(data)AjvValidationError
Error thrown when validation fails.
import { AjvValidationError } from '@kontract/adonis'
try {
validate(schema, data)
} catch (error) {
if (error instanceof AjvValidationError) {
error.status // 422
error.code // 'E_VALIDATION_ERROR'
error.errors // [{ field: 'email', message: '...' }]
}
}Serializers
Built-in serializers for Lucid models and paginators.
Type Guards
import {
isLucidModel, // Has serialize() method
isTypedModel, // Has toResponse() method
isPaginator, // Lucid paginator object
hasSerialize, // Generic serialize check
} from '@kontract/adonis'
if (isTypedModel(data)) {
return data.toResponse()
}
if (isLucidModel(data)) {
return data.serialize()
}
if (isPaginator(data)) {
// { data: [...], meta: { total, perPage, currentPage, ... } }
}Serializer Registry
Serializers are ordered by priority (higher = checked first):
| Serializer | Priority | Checks |
|------------|----------|--------|
| paginatorSerializer | 150 | isPaginator() |
| typedModelSerializer | 100 | isTypedModel() |
| lucidModelSerializer | 50 | isLucidModel() |
| serializableSerializer | 25 | hasSerialize() |
import { lucidSerializers } from '@kontract/adonis'
// All serializers in priority order
lucidSerializersOpenAPI Builder
OpenApiBuilder
Generates OpenAPI specifications from decorated controllers.
import { OpenApiBuilder } from '@kontract/adonis'
const builder = new OpenApiBuilder({
title: 'My API',
description: 'API documentation',
version: '1.0.0',
servers: [
{ url: 'https://api.example.com', description: 'Production' },
{ url: 'http://localhost:3333', description: 'Development' },
],
openapiVersion: '3.1.0', // or '3.0.3'
securityScheme: {
name: 'BearerAuth',
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT access token',
},
})
const spec = builder.build()OpenApiBuilderOptions
interface OpenApiBuilderOptions {
title: string
description: string
version: string
servers: Array<{ url: string; description: string }>
openapiVersion?: '3.0.3' | '3.1.0' // default: '3.1.0'
securityScheme?: {
name: string
type: 'http' | 'apiKey' | 'oauth2'
scheme?: string
bearerFormat?: string
description?: string
}
}Generated Features
The builder automatically:
- Collects tags from
@Apidecorators - Converts paths (
:idto{id}) - Generates operationIds from method names
- Adds security for
auth: 'required'endpoints - Adds 401 response for authenticated endpoints
- Registers schemas in components
- Handles file uploads as multipart/form-data
OperationId Generation
For standard CRUD method names, operationIds are auto-generated:
| Method | Path | Generated operationId |
|--------|------|----------------------|
| index | GET /users | listUsers |
| show | GET /users/:id | getUser |
| store | POST /users | createUser |
| update | PUT /users/:id | updateUser |
| destroy | DELETE /users/:id | deleteUser |
Custom method names use the method name as operationId.
Re-exports
For convenience, the adapter re-exports common items from the core package:
import {
// Decorators
Api,
Endpoint,
// Response helpers
ok,
created,
accepted,
noContent,
badRequest,
unauthorized,
forbidden,
notFound,
conflict,
unprocessableEntity,
tooManyRequests,
internalServerError,
serviceUnavailable,
binary,
apiError,
// Configuration
defineConfig,
getConfig,
// Types
type ApiOptions,
type EndpointOptions,
type ApiResponse,
type BinaryResponse,
type EndpointMetadata,
type ApiMetadata,
} from '@kontract/adonis'Utilities
stripNestedIds(schema)
Removes $id from nested schemas to prevent AJV conflicts when the same schema is used multiple times.
import { stripNestedIds } from '@kontract/adonis'
const cleanedSchema = stripNestedIds(schema)getDefaultValidator()
Get or create the singleton AJV validator instance.
import { getDefaultValidator } from '@kontract/adonis'
const validator = getDefaultValidator()resetDefaultValidator()
Reset the default validator (useful for testing).
import { resetDefaultValidator } from '@kontract/adonis'
beforeEach(() => {
resetDefaultValidator()
})Integration Example
Complete example with AdonisJS:
// start/routes.ts
import router from '@adonisjs/core/services/router'
import { registerDecoratorRoutes, validate, OpenApiBuilder } from '@kontract/adonis'
// Import all controllers
const controllers = import.meta.glob('../app/controllers/**/*.ts', { eager: true })
// Register decorator-based routes
registerDecoratorRoutes(router, { validate })
// Serve OpenAPI spec
router.get('/docs/json', async ({ response }) => {
const builder = new OpenApiBuilder({
title: 'My API',
description: 'API documentation',
version: '1.0.0',
servers: [{ url: 'http://localhost:3333', description: 'Development' }],
})
return response.json(builder.build())
})Peer Dependencies
@adonisjs/core^6.0.0@sinclair/typebox>=0.32.0ajv^8.0.0 (optional, required for validation)@adonisjs/lucid^21.0.0 (optional, required for Lucid serializers)
License
MIT
