Getting started
This guide creates a small versioned API with one controller and a validated endpoint.
Requirements
- Node.js 22 or newer
- A TypeScript project using CommonJS-compatible output
experimentalDecoratorsandemitDecoratorMetadataenabled
Install Amala
Add Amala to an existing project:
npm install amala
Or generate a starter application:
npm create amala-app@latest my-api
The rest of this guide shows the manual setup so each moving part is visible.
Configure TypeScript
Use this as a baseline tsconfig.json:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"module": "commonjs",
"outDir": "dist",
"skipLibCheck": true,
"target": "ES2018"
},
"include": ["src/**/*.ts"]
}
Both decorator settings are required. Without emitted type metadata, Amala cannot transform and validate class-based handler arguments.
These settings select TypeScript's legacy decorator implementation. Standard decorators do not currently support parameter decorators such as @Body() or automatically emit the parameter types Amala needs.
Create a controller
Create src/controllers/HealthController.ts:
import {Controller, Get} from 'amala';
@Controller('/health')
export class HealthController {
@Get('/')
status() {
return {status: 'ok'};
}
}
Bootstrap the app
Create src/main.ts:
import {bootstrapControllers} from 'amala';
import {HealthController} from './controllers/HealthController';
async function start() {
const {app} = await bootstrapControllers({
attachRoutes: true,
basePath: '/api',
controllers: [HealthController],
useHelmet: true,
});
app.listen(3000, () => {
console.log('API listening at http://localhost:3000');
});
}
void start();
Compile and run the application with the scripts used by your project. Request:
GET http://localhost:3000/api/v1/health
The response is:
{"status":"ok"}
Version v1 is enabled by default. To serve GET /api/health, set disableVersioning: true.
Type application context
Koa accepts separate types for ctx.state and properties added directly to ctx. Amala preserves both:
import Koa from 'koa';
import {AmalaMiddleware, bootstrapControllers} from 'amala';
interface AppState {
user?: User;
services: Services;
}
interface ContextExtensions {
requestId: string;
}
const app = new Koa<AppState, ContextExtensions>();
const requestContext: AmalaMiddleware<AppState, ContextExtensions> =
async (ctx, next) => {
ctx.state.services = services;
ctx.requestId = crypto.randomUUID();
await next();
};
await bootstrapControllers({
app,
controllers: [HealthController],
flow: [requestContext],
});
The typed application lets bootstrapControllers infer both generic arguments. When Amala creates the app, provide them explicitly with bootstrapControllers<AppState, ContextExtensions>(...).
These generics catch accidental undeclared context access at compile time. They do not validate middleware output or establish an authenticated identity.
Add validation
Create src/controllers/UserController.ts:
import {
Body,
Controller,
IsEmail,
IsString,
Post,
} from 'amala';
class CreateUserInput {
@IsEmail()
email: string;
@IsString()
displayName: string;
}
@Controller('/users')
export class UserController {
@Post('/')
create(@Body({required: true}) input: CreateUserInput) {
return input;
}
}
Register UserController beside HealthController. Amala transforms the JSON body into CreateUserInput, runs class-validator, and returns 422 when validation fails.
For strict object contracts, pass class-validator options during bootstrap:
validatorOptions: {
forbidNonWhitelisted: true,
whitelist: true,
}
Add middleware
Use @Flow for controller- or endpoint-level Koa middleware:
const requireUser = async (ctx, next) => {
if (!ctx.state.user) {
ctx.throw(401, 'Authentication required');
}
await next();
};
@Post('/')
@Flow(requireUser)
create(@Body({required: true}) input: CreateUserInput) {
return input;
}
Authentication and authorization are not built into Amala. Your middleware must establish identity and enforce access before the handler runs.
OpenAPI and Swagger
OpenAPI is enabled by default. With basePath: '/api', Amala serves:
- OpenAPI JSON at
GET /api/docs - Swagger UI at
GET /api/swagger
Set a public origin when the generated server URLs need an absolute URL:
openAPI: {
enabled: true,
publicURL: 'https://api.example.com',
}
An omitted publicURL keeps Swagger on the same origin. Disable or protect these endpoints when your route inventory should not be public.
Attach routes manually
attachRoutes is false by default. That lets you insert your own middleware before the generated router:
const {app, router} = await bootstrapControllers({
basePath: '/api',
controllers: [HealthController, UserController],
});
app.use(yourMiddleware);
app.use(router.routes());
app.use(router.allowedMethods());
Continue with the bootstrapControllers reference and the production security guide.