⚠️ EXPERIMENTAL: This library is in early development and highly experimental. APIs may change without notice. Not recommended for production use.
Oakest is a decorator-driven application toolkit for Deno's oak.
It provides controllers, modules, explicit dependency injection, middleware decorators, and route argument resolvers in a small API surface built around standard decorators.
The current API uses standard decorators, explicit dependency injection via inject(...), and route argument resolvers on @Get/@Post/....
Project background: Oakest was originally based on biga816/oak-decorators.
- Controllers and Modules: Organize Oak routes in a clear application structure.
- Explicit Dependency Injection: Declare dependencies with
inject(...)instead of emitted type metadata. - Standard Decorators: Build on the current TC39 decorator model instead of legacy experimental decorators.
- Middleware Decorators: Attach reusable request guards and flow control directly to route methods.
- Route Argument Resolvers: Map params, body, query, headers, request, response, context, or custom values directly on route decorators.
Define controllers to handle HTTP endpoints
// ./controllers/util-controller.ts
import { Controller, Get, headers, query } from '@dx/oakest';
@Controller('util')
export class UtilController {
@Get('user-agent', [headers<string>('user-agent')])
bounceUserAgent(userAgent: string) {
return { status: 'ok', userAgent };
}
@Get('multiply', [query<string>('f1'), query<string>('f2')])
getRandomStuff(factor1: string, factor2: string) {
return { status: 'ok', result: Number(factor1) * Number(factor2) };
}
}Define modules
// ./app.module.ts
import { Module } from '@dx/oakest';
import { UtilController } from './controllers/util-controller.ts';
@Module({
controllers: [UtilController],
routePrefix: 'api/v1',
modules: [], // optional submodules
})
export class AppModule {}Register an app module with oak.
// ./main.ts
import { Application } from '@oak/oak';
import { assignModule } from '@dx/oakest';
import { AppModule } from './app.module.ts';
const app = new Application();
app.use(assignModule(AppModule));
await app.listen({ port: 8000 });Run your app and the following endpoints will be available:
/api/v1/util/user-agent/api/v1/util/multiply?f1=2&f2=4
If you are upgrading from older Oakest releases, these are the important changes:
reflect-metadatais no longer used.emitDecoratorMetadatais no longer required.- Constructor dependencies must now be declared explicitly with
inject(...). @Controller({ injectables: [...] })has been removed.@Injectable({ isSingleton: false })is no longer supported in the current Needle-based DI flow.- Parameter decorators like
@Body(),@Param(),@Query(),@Headers(),@Req(), and@Ctx()have been removed. - Route inputs now belong on
@Get/@Post/...via resolver arrays like@Post(':id', [param('id'), body()]). - Custom middleware decorators must use the standard decorator context form
(_value, context). experimentalDecoratorsis no longer needed indeno.json.
Migration checklist:
- Replace implicit constructor injection with
inject(...)default values. - Keep all injectable implementations in the corresponding module
providersarrays. - Remove
@Controller({ injectables: [...] })and move token selection into explicit constructor injection. - Replace parameter decorators with route argument resolvers on
@Get/@Post/.... - Remove code or config that depended on
reflect-metadataor emitted constructor metadata. - Migrate custom middleware decorators from
(target, methodName)to(_value, context)and callregisterMiddlewareMethodDecorator(context, handler).
Constructor migration example:
import { Controller, Get, inject } from '@dx/oakest';
import { UsersService } from './users.service.ts';
@Controller('users')
export class UsersController {
constructor(private readonly usersService = inject(UsersService)) {}
@Get()
getAllUsers() {
return this.usersService.getAllUsers();
}
}Route argument migration example:
import { Controller, Get, param, query } from '@dx/oakest';
@Controller('users')
export class UsersController {
@Get(':id', [param<string>('id'), query<string | null>('expand')])
findOne(id: string, expand: string | null) {
return { id, expand };
}
}Common upgrade failures:
No provider(s) found: the dependency is requested withinject(...), but the implementation is missing from the moduleprovidersarray.- Token-based injection does not resolve: the provider is missing
@Injectable({ implementing: TOKEN }), or the constructor is not usinginject<T>(TOKEN). - Constructor injection silently stopped working after the upgrade: the constructor was not converted to explicit
inject(...)style.
A module is a class annotated with a @Module() decorator. The @Module() decorator provides metadata that the application makes use of to organize the application structure.
Each application has at least one module, a root module, and each modules can have child modules.
The @Module() decorator takes those options:
| name | description |
|---|---|
controllers |
the set of controllers defined in this module which have to be instantiated |
providers |
the providers that will be instantiated by the injector |
modules |
the set of modules defined as child modules of this module |
routePrefix |
the prefix name to be set in route as the common URL for controllers. |
import { Module } from '@dx/oakest';
import { AppController } from './app.controller.ts';
import { SampleModule } from './sample/sample.module.ts';
@Module({
modules: [SampleModule],
controllers: [AppController],
routePrefix: 'v1',
})
export class AppModule {}A controller is a class annotated with a @Controller() decorator. Controllers are responsible for handling incoming requests and returning responses to the client.
The @Controller() decorator takes an optional route path prefix.
import { Controller, Get } from '@dx/oakest';
@Controller('sample')
export class UsersController {
@Get()
findAll(): string {
return 'OK';
}
}The @Get() HTTP request method decorator before the findAll() method tells the application to create a handler for a specific endpoint for HTTP requests.
For http methods, you can use @Get(), @Post(), @Put(), @Patch(), @Delete(), @All().
Handlers can map request-derived values directly on the HTTP method decorator.
import { Controller, Get, headers, param, query } from '@dx/oakest';
@Controller('sample')
export class SampleController {
@Get(':id', [param<string>('id'), query<string | null>('dryRun'), headers<string>('user-agent')])
findOne(id: string, dryRun: string | null, userAgent: string) {
return { id, dryRun, userAgent };
}
}Available resolvers:
| name | result |
|---|---|
req() |
context.request |
res() |
context.response |
next() |
Oak next handler |
query(key?) |
URLSearchParams or a single query value |
param(key?) |
route params object or a single route param |
body(key?) |
parsed JSON body or a single body property |
headers(name?) |
all headers as an object or a single header value |
ip() |
client IP |
ctx() |
full Oak router context |
custom(handler, data?) |
custom async/sync value resolver, typed from the handler return value |
If the handler declares exactly one parameter and no resolver array, Oakest still injects ctx automatically.
If the handler uses a resolver array and declares exactly one extra trailing parameter, that final parameter receives ctx automatically.
Providers are responsible for main business logic as services, repositories, factories, helpers, and so on. The main idea of a provider is that it can be injected as a dependency. Depending on the environment, different implementations of a service can be provided.
// ./sample.service.ts
import { Injectable } from '@dx/oakest';
import db from './db-service.ts';
@Injectable()
export class UserService {
async getAllUsers() {
const { data: users } = await db.users.getAll();
return { status: 'ok', data: users };
}
}
@Injectable()
export class MockUserService {
getAllUsers() {
return {
status: 'ok',
data: [
{
name: 'John Doe',
},
{
name: 'Jane Doe',
},
],
};
}
}
// ./sample.controller.ts
import { Controller, Get, inject } from '@dx/oakest';
import { UserService } from './sample.service.ts';
@Controller('users')
export class UsersController {
constructor(private readonly userService = inject(UserService)) {}
@Get()
getAllUsers() {
return this.userService.getAllUsers();
}
}
// ./sample.module.ts
import { Module } from '@dx/oakest';
import { UsersController } from './sample.controller.ts';
import { MockUserService, UserService } from './sample.service.ts';
@Module({
controllers: [UsersController],
providers: [
Deno.env.get('DENO_ENV') === 'production' ? UserService : MockUserService,
],
})
export class SampleModule {}Dependency injection notes:
- Dependencies must be requested explicitly in constructor default values.
- Providers still need to be registered in your module's
providersarray. @Injectable({ implementing: TOKEN })can still be used to bind string or symbol tokens and resolve them withinject<T>(TOKEN).isSingleton: falseis no longer supported in this Needle-based mode.@Controller({ injectables: [...] })is no longer part of the public API.experimentalDecoratorsis no longer needed indeno.json.
It's possible to register middleware that can be used in controllers by means of decorators.
For instance, to protect routes based on user roles, you can create a @RequiresRole middleware decorator.
// ./middleware.ts
import { registerMiddlewareMethodDecorator } from '@dx/oakest';
import type { Context } from '@oak/oak';
function checkUserRoles(context: Context, roles: string[]) {
// Logic to check the user role
return false;
}
export function RequiresRole(roles: string[]) {
return function (_value, context) {
const requiresRole = async (context, next) => {
// Logic to check the user session or JWT for the required role
if (checkUserRoles(context, roles)) {
await next();
} else {
// handle unauthorized access
context.response.status = 401;
context.response.body = { error: 'Unauthorized' };
return;
}
};
registerMiddlewareMethodDecorator(context, requiresRole);
};
}Then you can use the @RequiresRole decorator in your controllers' methods.
// ./sample.controller.ts
import { Controller, Get } from '@dx/oakest';
import { RequiresRole } from './middleware.ts';
@Controller('users')
export class SampleController {
@Get('/')
@RequiresRole(['admin'])
getAllUsers() {
// Logic to get all users
}
}If you already had custom middleware decorators in your codebase, the required migration is just the decorator signature change from (target, methodName) to (_value, context).
Custom route inputs can be declared inline with custom(...). The resolver type is inferred from the handler return value.
import { Controller, custom, Get } from '@dx/oakest';
@Controller('users')
export class UsersController {
@Get('me', [custom((ctx) => ctx.state.jwtData?.sub)])
getCurrentUser(userId: string | undefined) {
return { userId };
}
}Resolvers can be asynchronous too:
import { Controller, custom, Get } from '@dx/oakest';
@Controller('products')
export class ProductsController {
@Get('recent', [custom(async (ctx) => {
return ctx.state.jwtData?.sid ? await retrieveSession(ctx.state.jwtData.sid) : null;
})])
getRecentProducts(sessionData: { recentProducts: unknown[] } | null) {
return sessionData?.recentProducts ?? [];
}
}