adorn-api
v1.0.19
Published
Stage-3 decorator-first OpenAPI + routing toolkit
Readme
Adorn-API
A Stage-3 decorator-first OpenAPI + routing toolkit for Express with full TypeScript support.
Features
- Decorators-First: Use Stage-3 decorators to define controllers, routes, middleware, and auth
- Auto-Generated OpenAPI: OpenAPI 3.1 specs generated from your TypeScript types
- Swagger UI: Interactive API documentation at
/docswith zero configuration - Type-Safe: Full TypeScript inference throughout your API
- Authentication: Built-in auth support with scope-based authorization
- Middleware: Apply middleware globally, per-controller, or per-route
- Metal-ORM Integration: Seamless database integration with type-safe queries
- Validation: AJV runtime validation with optional precompiled validators
- Hot Reload: Development mode with automatic rebuilds
Installation
npm install adorn-api
npm install -D @types/expressQuick Start
// controller.ts
import { Controller, Get, Post } from "adorn-api";
interface User {
id: number;
name: string;
email: string;
}
@Controller("/users")
export class UserController {
@Get("/")
async getUsers(): Promise<User[]> {
return [{ id: 1, name: "Alice", email: "[email protected]" }];
}
@Post("/")
async createUser(body: { name: string; email: string }): Promise<User> {
return { id: 2, name: body.name, email: body.email };
}
}// server.ts
import { bootstrap } from "adorn-api/express";
import { UserController } from "./controller.js";
await bootstrap({
controllers: [UserController],
});Run with:
npx adorn-api devOpen http://localhost:3000/docs to see your Swagger UI documentation.
Core Concepts
Controllers
Define a controller with a base path:
@Controller("/api/users")
export class UserController {}Route Handlers
Use HTTP method decorators to define routes:
@Controller("/users")
export class UserController {
@Get("/")
async list(): Promise<User[]> {}
@Get("/:id")
async get(id: number): Promise<User> {}
@Post("/")
async create(body: CreateUserDto): Promise<User> {}
@Put("/:id")
async update(id: number, body: UpdateUserDto): Promise<User> {}
@Patch("/:id")
async patch(id: number, body: Partial<User>): Promise<User> {}
@Delete("/:id")
async delete(id: number): Promise<void> {}
}Parameters
Parameters are automatically extracted from your handler signature:
async handler(
id: number, // Path parameter
query: { limit?: number; sort?: string }, // Query parameters
body: CreateUserDto, // Request body
headers: { authorization?: string }, // Headers
cookies: { sessionId?: string } // Cookies
) {}Middleware
Apply middleware at any level:
// Global middleware
const app = await bootstrap({
controllers: [UserController],
middleware: {
global: [loggingMiddleware, corsMiddleware],
named: { auth: authMiddleware },
},
});
// Controller-level middleware
@Controller("/users")
@Use(authMiddleware)
export class UserController {}
// Route-level middleware
@Get("/admin")
@Use(adminMiddleware)
async adminOnly() {}
// Named middleware
@Get("/protected")
@Use("auth")
async protected() {}Middleware executes in order: global → controller → route → handler.
Authentication
Define auth schemes and protect routes:
import { Auth, Public } from "adorn-api";
// Define auth scheme
const bearerRuntime = {
name: "BearerAuth",
async authenticate(req: any) {
const token = req.headers.authorization?.replace("Bearer ", "");
const user = await verifyToken(token);
return user ? { principal: user, scopes: user.scopes } : null;
},
challenge(res: any) {
res.status(401).json({ error: "Unauthorized" });
},
authorize(auth: any, requiredScopes: string[]) {
return requiredScopes.every(s => auth.scopes?.includes(s));
},
};
// Bootstrap with auth
await bootstrap({
controllers: [UserController],
auth: {
schemes: { BearerAuth: bearerRuntime },
},
});
// Protect routes
@Controller("/api")
export class ApiController {
@Get("/public")
@Public()
async publicEndpoint() {}
@Get("/profile")
@Auth("BearerAuth")
async getProfile() {}
@Post("/admin")
@Auth("BearerAuth", { scopes: ["admin"] })
async adminOnly() {}
}Optional Authentication
@Get("/resource")
@Auth("BearerAuth", { optional: true })
async getResource(req: any) {
if (req.auth) {
return { user: req.auth.principal };
}
return { user: null };
}Metal-ORM Integration
Seamlessly integrate with Metal-ORM for database operations:
import { Controller, Get } from "adorn-api";
import type { ListQuery } from "adorn-api/metal";
import { applyListQuery } from "adorn-api/metal";
import { selectFromEntity, entityRef } from "metal-orm";
@Controller("/tasks")
export class TasksController {
@Get("/")
async list(query: ListQuery<Task>): Promise<PaginatedResult<Task>> {
const session = getSession();
const T = entityRef(Task);
const qb = selectFromEntity(Task)
.select("id", "title", "completed")
.where(eq(T.completed, false));
return applyListQuery(qb, session, query);
}
}ListQuery supports:
- Pagination:
page,perPage - Sorting:
sort(string or array, prefix with-for DESC) - Filtering:
where(deep object filters)
Register Metal Entities
Auto-generate OpenAPI schemas from Metal-ORM entities:
import { registerMetalEntities } from "adorn-api/metal";
import { User, Post, Comment } from "./entities/index.js";
registerMetalEntities(openapi, [User, Post, Comment], {
mode: "read",
stripEntitySuffix: true,
includeRelations: "inline",
});Examples
The repository includes several examples demonstrating different features:
Basic
Simple CRUD API with GET, POST endpoints and in-memory data.
npm run example basicSimple Auth
Authentication with bearer tokens, scope-based authorization, public/protected endpoints.
npm run example simple-authTask Manager
Complete task management API with SQLite3, filtering, tags, and statistics.
npm run example task-managerThree Controllers
Multiple controllers (Users, Posts, Comments) in a blog application.
npm run example three-controllersBlog Platform (Metal-ORM)
Full-featured blog platform with Metal-ORM, relationships, and advanced queries.
npm run example blog-platform-metal-ormE-commerce
E-commerce API with RESTful and non-RESTful endpoints, carts, orders, and coupons.
npm run example ecommerceSimple Pagination (Metal-ORM)
Pagination and sorting with Metal-ORM integration.
npm run example simple-pagination-metal-ormQuery JSON (Metal-ORM)
Advanced filtering with deep object query parameters.
npm run example query-json-metal-ormCLI
Development
npx adorn-api devBuilds artifacts and starts server with hot-reload.
Build
npx adorn-api buildGenerates .adorn/ directory with:
openapi.json- OpenAPI 3.1 specificationmanifest.json- Runtime binding metadatacache.json- Build cache for incremental rebuildsvalidator.js- Precompiled validators (if enabled)
Run Examples
# List all examples
npm run example:list
# Run specific example
npm run example basic
npm run example blog-platform-metal-ormAPI Reference
Decorators
@Controller(path)- Define a controller with base path@Get(path)- GET route handler@Post(path)- POST route handler@Put(path)- PUT route handler@Patch(path)- PATCH route handler@Delete(path)- DELETE route handler@Use(...middleware)- Apply middleware@Auth(scheme, options)- Require authentication@Public()- Mark route as public (bypasses auth)
Exports
import {
Controller,
Get,
Post,
Put,
Patch,
Delete,
Use,
Auth,
Public,
} from "adorn-api";
import {
bootstrap,
createExpressRouter,
setupSwagger,
} from "adorn-api/express";
import {
ListQuery,
applyListQuery,
registerMetalEntities,
} from "adorn-api/metal";
import { readAdornBucket } from "adorn-api";
import type { AdornBucket, AuthSchemeRuntime, AuthResult } from "adorn-api";Bootstrap Options
await bootstrap({
controllers: [UserController, PostController],
auth: {
schemes: {
BearerAuth: bearerRuntime,
ApiKey: apiKeyRuntime,
},
},
middleware: {
global: [logger, cors],
named: { auth: authMiddleware },
},
port: 3000,
host: "0.0.0.0",
});Auth Scheme
const authScheme: AuthSchemeRuntime = {
name: "MyAuth",
async authenticate(req: any) {
return { principal: user, scopes: ["read", "write"] };
},
challenge(res: any) {
res.status(401).json({ error: "Unauthorized" });
},
authorize(auth: any, requiredScopes: string[]) {
return requiredScopes.every(s => auth.scopes?.includes(s));
},
};Validation
Adorn-API supports two validation modes:
Runtime Validation (AJV)
await bootstrap({
controllers: [UserController],
validation: {
mode: "ajv-runtime",
},
});Precompiled Validators
await bootstrap({
controllers: [UserController],
validation: {
mode: "precompiled",
},
});Precompiled validators are generated at build time in .adorn/validator.js for better performance.
Testing
Tests are written with Vitest and cover:
- Compiler schema generation
- Decorator metadata
- Express integration
- Middleware execution order
- Authentication and authorization
- Metal-ORM integration
npm testTest Structure
test/
├── integration/ # Express integration tests
├── compiler/ # Schema and manifest generation
├── runtime/ # Decorator metadata
├── middleware/ # Middleware ordering and auth
├── metal/ # Metal-ORM integration
└── fixtures/ # Test fixturesConfiguration
TypeScript Config
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Vitest Config
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["test/**/*.test.ts"],
typecheck: {
enabled: true,
tsconfig: "./tsconfig.json",
},
},
});How It Works
- Compile: The CLI analyzes your TypeScript controllers and extracts metadata using the compiler API
- Generate: OpenAPI schemas and runtime manifests are generated from type information
- Bind: At runtime, metadata is merged with controller instances to bind routes to Express
- Validate: Optional validation ensures requests match your TypeScript types
- Document: Swagger UI serves interactive documentation based on generated OpenAPI spec
License
MIT
