Skip to content

thomas3577/oakest

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

93 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Oakest

JSR Version JSR Score ci

⚠️ 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.

Highlights

  • 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.

Quick Start

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

Upgrade Notes

If you are upgrading from older Oakest releases, these are the important changes:

  • reflect-metadata is no longer used.
  • emitDecoratorMetadata is 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).
  • experimentalDecorators is no longer needed in deno.json.

Migration checklist:

  1. Replace implicit constructor injection with inject(...) default values.
  2. Keep all injectable implementations in the corresponding module providers arrays.
  3. Remove @Controller({ injectables: [...] }) and move token selection into explicit constructor injection.
  4. Replace parameter decorators with route argument resolvers on @Get/@Post/....
  5. Remove code or config that depended on reflect-metadata or emitted constructor metadata.
  6. Migrate custom middleware decorators from (target, methodName) to (_value, context) and call registerMiddlewareMethodDecorator(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 with inject(...), but the implementation is missing from the module providers array.
  • Token-based injection does not resolve: the provider is missing @Injectable({ implementing: TOKEN }), or the constructor is not using inject<T>(TOKEN).
  • Constructor injection silently stopped working after the upgrade: the constructor was not converted to explicit inject(...) style.

API Overview

Modules

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 {}

Controllers

Routing

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().

Route arguments

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

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 providers array.
  • @Injectable({ implementing: TOKEN }) can still be used to bind string or symbol tokens and resolve them with inject<T>(TOKEN).
  • isSingleton: false is no longer supported in this Needle-based mode.
  • @Controller({ injectables: [...] }) is no longer part of the public API.
  • experimentalDecorators is no longer needed in deno.json.

Custom Middleware Decorators

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 argument resolvers

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 ?? [];
  }
}

About

Project implements decorators for oak like Nest.js.

Resources

License

Stars

Watchers

Forks

Contributors