This is a fork of biga816/oak-decorators
NestJS-style decorators library for Deno's oak.
For more info check this issue
Define controllers to handle HTTP endpoints
// ./controllers/util-controller.ts import { Controller, Get, Headers } from '@dx/oakest'; @Controller('util') export class UtilController { @Get('user-agent') bounceUserAgent(@Headers('user-agent') userAgent: string) { return { status: 'ok', userAgent }; } @Get('multiply') getRandomStuff(@Query('f1') factor1: number, @Query('f2') factor2: number) { return { status: 'ok', result: factor1 * factor2 }; } }
Define modules
// ./app.module.ts import { Module } from '@dx/oakest'; import { UtilController } from './app.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 following endpoints will be available:
/api/v1/util/user-agent
/api/v1/util/multiply?f1=2&f2=4
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 ULR 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 take a route path prefix optionally.
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 often need access to the client request details.
HHere's a example to access the request object using @Req()
decorator.
import { Controller, Get, Request } from '@dx/oakest'; @Controller('sample') export class SampleController { @Get() findAll(@Request() request: Request): string { return 'OK'; } }
Below is a list of the provided decorators.
name |
---|
| @Request()
| @Response()
| @Next()
| @Query(key?: string)
| @Param(key?: string)
| @Body(key?: string)
| @Headers(name?: string)
| @Ip()
| @Context()
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 { error, 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 } from '@dx/oakest'; import { UserService } from './sample.service.ts'; @Controller('users') export class UsersController { constructor(private readonly userService: UserService) {} @Get() getAllUsers() { return await 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 {}
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 { 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 (target, methodName) { 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(target, methodName, requiresRole); }; }
Then you can use the @RequiresRole
decorator in your controllers's methods.
// ./sample.controller.ts import RequireRole from './middleware.ts'; @Controller('users') export default class SampleController { @Get('/') @RequiresRole(['admin']) getAllUsers() { // Logic to get all users } }
It's also possible to register custom parameters decorators to streamline data injection into endpoint handlers
It would be useful to have a shortcut to some data stored in the request's JWT.
The approach would involve having a high priority controller that parses the JWT and stores it in context.state.jwtData
.
Then a param decorator could be defined as follows:
export function JWT(propName?: string) { return function (targetClass: any, methodName: string, paramIndex: number) { const handler = (ctx: Context) => propName ? ctx.state.jwtData?.[propName] : ctx.state.jwtData; registerCustomRouteParamDecorator( targetClass, methodName, paramIndex, )(handler); }; }
And used in controllers like this:
// sample-controller @Get('my-subscriptions') getUserSubscriptions(@JWT('sub') userId : string) { return await databaseService.getUserSubscriptions(userId); }
Params resolution is asynchronous, so it is also possible to do things like retrieving session information from KV stores on demand. This would be a more efficient strategy than having a middleware that always retrieves session data if this is not desirable.
export function SessionData() { return function (targetClass: any, methodName: string, paramIndex: number) { const handler = (ctx: Context) => ctx.state.jwtData?.sid ? await retrieveSession(ctx.state.jwtData?.sid) : null; registerCustomRouteParamDecorator( targetClass, methodName, paramIndex, )(handler); }; }
// sample-controller @Get('my-recent-products') getUserSubscriptions(@SessionData() sessionData : any) { return sessionData.recentProducts }
Add Package
deno add jsr:@dx/oakest
Import symbol
import * as oakest from "@dx/oakest";
Import directly with a jsr specifier
import * as oakest from "jsr:@dx/oakest";