npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kontract

v0.4.0

Published

Framework-agnostic type-safe API contracts with TypeBox schema support

Readme

kontract

Framework-agnostic OpenAPI decorator system with TypeBox schema support.

Features

  • Unified @Endpoint decorator - Define route, auth, schemas, and responses in one place
  • Type-safe response helpers - ok(), created(), notFound() with compile-time validation
  • Framework-agnostic - Core library has no framework dependencies
  • OpenAPI 3.1.0 & 3.0.3 - Generate specs for either version
  • TypeBox integration - Native support for TypeBox schemas

Installation

For AdonisJS projects, use the adapter package instead (it includes this package):

npm install @kontract/adonis @sinclair/typebox ajv ajv-formats

For other frameworks or custom integrations:

npm install kontract @sinclair/typebox

Quick Start

import { Api, Endpoint, ok, apiError } from 'kontract'
import { Type, Static } from '@sinclair/typebox'

// 1. Define your schemas
const User = Type.Object({
  id: Type.Number(),
  name: Type.String(),
  email: Type.String({ format: 'email' }),
}, { $id: 'User' })

type UserType = Static<typeof User>

// 2. Decorate your controllers
@Api({ tag: 'Users', description: 'User management endpoints' })
class UsersController {
  @Endpoint('GET /api/v1/users/:id', {
    summary: 'Get a user by ID',
    params: Type.Object({ id: Type.String() }),
    responses: {
      200: { schema: User, description: 'The user' },
      404: null,
    },
  })
  async show(ctx: unknown, body: unknown, query: unknown, params: { id: string }) {
    const user = await findUser(params.id)
    if (!user) {
      return apiError.notFound('User not found')
    }
    return ok(User, user)
  }
}

Decorators

@Api(options)

Class decorator for controllers. Groups endpoints under an OpenAPI tag.

interface ApiOptions {
  tag: string           // OpenAPI tag for grouping endpoints
  description?: string  // Description of the API group
  prefix?: string       // Optional path prefix for all endpoints
}

Example:

@Api({
  tag: 'Books',
  description: 'Book management endpoints',
  prefix: '/api/v1'
})
class BooksController { }

@Endpoint(route, options)

Method decorator for endpoints. Defines the route, validation schemas, and OpenAPI documentation.

interface EndpointOptions {
  summary?: string           // Short summary for OpenAPI docs
  description?: string       // Detailed description
  operationId?: string       // Unique operation ID (auto-generated if not provided)
  deprecated?: boolean       // Mark endpoint as deprecated
  auth?: 'required' | 'optional' | 'none'  // Authentication requirement
  body?: TSchema             // Request body schema (TypeBox)
  query?: TSchema            // Query parameters schema
  params?: TSchema           // Path parameters schema
  file?: FileUploadConfig    // File upload configuration
  responses: Record<number, TSchema | null | { schema: TSchema | null; description?: string }>
  middleware?: unknown[]     // Framework-specific middleware
}

Examples:

// GET with path parameters
@Endpoint('GET /api/v1/books/:id', {
  summary: 'Get a book by ID',
  params: Type.Object({ id: Type.String({ format: 'uuid' }) }),
  responses: {
    200: { schema: Book, description: 'The book' },
    404: null,
  },
})
async show() { }

// POST with body and authentication
@Endpoint('POST /api/v1/books', {
  summary: 'Create a new book',
  auth: 'required',
  body: CreateBookRequest,
  responses: {
    201: { schema: Book, description: 'Book created' },
    422: { schema: ValidationError },
  },
})
async store() { }

// GET with query parameters
@Endpoint('GET /api/v1/books', {
  summary: 'List books',
  query: Type.Object({
    page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),
    limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),
    search: Type.Optional(Type.String()),
  }),
  responses: {
    200: { schema: BookListResponse },
  },
})
async index() { }

// DELETE with no response body
@Endpoint('DELETE /api/v1/books/:id', {
  summary: 'Delete a book',
  auth: 'required',
  params: Type.Object({ id: Type.String() }),
  responses: {
    204: null,
    404: null,
  },
})
async destroy() { }

// File upload
@Endpoint('POST /api/v1/books/:id/cover', {
  summary: 'Upload book cover',
  auth: 'required',
  file: { fieldName: 'cover', multiple: false },
  responses: {
    200: { schema: Book },
  },
})
async uploadCover() { }

Response Helpers

Response helpers create typed API responses. They return a structured object with status and data properties.

Success Responses

import { ok, created, accepted, noContent } from 'kontract'

// 200 OK
return ok(UserSchema, { id: 1, name: 'John' })

// 201 Created
return created(UserSchema, { id: 1, name: 'John' })

// 202 Accepted
return accepted(JobSchema, { jobId: 'abc123' })

// 204 No Content
return noContent()

Error Responses

For full control over error response data:

import {
  badRequest,
  unauthorized,
  forbidden,
  notFound,
  conflict,
  unprocessableEntity,
  tooManyRequests,
  internalServerError,
  badGateway,
  serviceUnavailable
} from 'kontract'

// All error helpers follow the same pattern: (schema, data)
return notFound(ErrorSchema, { message: 'Book not found' })
return unauthorized(ErrorSchema, { message: 'Invalid token' })

Unified apiError Helper

For common error patterns with sensible defaults. Uses a standard ApiErrorBody structure:

import { apiError } from 'kontract'

// Use defaults
return apiError.notFound()              // "Resource not found"
return apiError.unauthorized()          // "Authentication required"
return apiError.forbidden()             // "Access denied"

// Override message
return apiError.notFound('Book not found')
return apiError.serviceUnavailable('External API is down')

// Validation errors with field details
return apiError.validation([
  { field: 'email', message: 'Invalid email format' },
  { field: 'age', message: 'Must be a positive number' },
])

Available methods: | Method | Status | Default Message | |--------|--------|-----------------| | apiError.badRequest(msg?) | 400 | "Bad request" | | apiError.unauthorized(msg?) | 401 | "Authentication required" | | apiError.forbidden(msg?) | 403 | "Access denied" | | apiError.notFound(msg?) | 404 | "Resource not found" | | apiError.conflict(msg?) | 409 | "Resource conflict" | | apiError.validation(errors) | 422 | "Validation failed" | | apiError.rateLimited(msg?) | 429 | "Too many requests" | | apiError.internal(msg?) | 500 | "Internal server error" | | apiError.serviceUnavailable(msg?) | 503 | "Service unavailable" | | apiError.externalApi(msg?) | 502 | "External API error" |

Binary Responses

For file downloads:

import { binary } from 'kontract'

// Return a file download
return binary(200, 'application/pdf', pdfBuffer, 'report.pdf')
return binary(200, 'image/png', imageBuffer)

Metadata Access

Access registered decorator metadata programmatically:

import {
  getRegisteredControllers,
  getApiMetadata,
  getEndpointMetadata,
  getControllerMetadata,
  getAllControllerMetadata,
  clearRegistry,
} from 'kontract'

// Get all registered controllers
const controllers = getRegisteredControllers()

// Get @Api metadata for a specific controller
const apiMeta = getApiMetadata(UsersController)
// { tag: 'Users', description: 'User management' }

// Get all @Endpoint metadata for a controller
const endpoints = getEndpointMetadata(UsersController)
// EndpointMetadata[]

// Get combined metadata
const metadata = getControllerMetadata(UsersController)
// { controller, api: ApiMetadata, endpoints: EndpointMetadata[] }

// Get all metadata at once
const allMetadata = getAllControllerMetadata()
// Array of { controller, api, endpoints }

// Clear registry (useful for testing)
clearRegistry()

Error Classes

RequestValidationError

Thrown when request validation fails:

import { RequestValidationError } from 'kontract'

try {
  validate(schema, data)
} catch (error) {
  if (error instanceof RequestValidationError) {
    error.status   // 422
    error.code     // 'E_VALIDATION_ERROR'
    error.errors   // [{ field: 'email', message: '...' }]
    error.source   // 'body' | 'query' | 'params'
    error.schema   // The TypeBox schema that failed
    error.data     // The data that was validated

    // Get response-ready format
    const response = error.toResponse()
    // { status: 422, code: 'E_VALIDATION_ERROR', message: 'Validation failed', errors: [...] }
  }
}

ResponseValidationError

Thrown when response validation fails (development mode only):

import { ResponseValidationError } from 'kontract'

Configuration Errors

import {
  ConfigurationError,      // General configuration issue
  AdapterNotFoundError,    // Framework adapter not found
  SerializerNotFoundError  // Serializer not found for data type
} from 'kontract'

Types

Response Types

import type {
  ApiResponse,      // { status: number, data: T }
  BinaryResponse,   // { status: number, binary: true, contentType: string, data: Buffer, filename?: string }
  AnyResponse,      // ApiResponse | BinaryResponse
  ApiErrorBody,     // { status: number, code: string, message: string, errors?: [...] }
  ErrorCode,        // Error code string type
} from 'kontract'

import { ErrorCodes, isBinaryResponse } from 'kontract'

// Check if response is binary
if (isBinaryResponse(result)) {
  // result.contentType, result.data, result.filename
}

Metadata Types

import type {
  HttpMethod,          // 'get' | 'post' | 'put' | 'patch' | 'delete'
  AuthLevel,           // 'required' | 'optional' | 'none'
  RouteString,         // 'GET /path' format
  ResponseDefinition,  // { schema: TSchema | null, description?: string }
  FileUploadConfig,    // { fieldName: string, multiple?: boolean }
  ApiMetadata,         // @Api decorator metadata
  EndpointMetadata,    // @Endpoint decorator metadata
  ControllerMetadata,  // Combined controller metadata
} from 'kontract'

OpenAPI Types

import type {
  OpenApiVersion,        // '3.0.3' | '3.1.0'
  OpenApiDocument,       // Full OpenAPI specification document
  OpenApiPathItem,       // Path item object
  OpenApiOperation,      // Operation object
  OpenApiParameter,      // Parameter object
  OpenApiRequestBody,    // Request body object
  OpenApiResponse,       // Response object
  OpenApiMediaType,      // Media type object
  OpenApiSchema,         // Schema object
  OpenApiSecurityScheme, // Security scheme object
} from 'kontract'

Validation Types

import type {
  Validator,           // Validator interface
  CompiledValidator,   // Pre-compiled validator for performance
  ValidatorOptions,    // Validator configuration options
  ValidationErrorDetail, // { field: string, message: string }
} from 'kontract'

Runtime/Adapter Types

import type {
  RequestContext,      // Generic request context
  AuthUser,            // Authenticated user type
  AuthResult,          // Authentication result
  RouteHandler,        // Route handler function
  RouterAdapter,       // Router adapter interface
  AuthAdapter,         // Authentication adapter interface
  ContainerAdapter,    // DI container adapter interface
  ResponseAdapter,     // Response adapter interface
  LoggerAdapter,       // Logger adapter interface
  FrameworkAdapters,   // All adapters combined
} from 'kontract'

Framework Adapters

This is the core library. For framework-specific implementations, see:

License

MIT