Options
All
  • Public
  • Public/Protected
  • All
Menu

typed-http-decorators

typed-http-decorators

Deploy Coverage Status

Typesafe decorators for HTTP endpoints which integrates nicely with Nest.js.

Installation

npm i typed-http-decorators

Usage

Decorate endpoints:

// Source: main.ts

/*
!!! You must import your decorator logic before applying typed-http-decorators !!!
*/
import './overrides';

import { Method, NotFound, Ok, t } from 'typed-http-decorators';

class NotFoundDto {
constructor(public message: string) {}
}

class ResourceDto {
constructor(public id: string, public name: string) {}
}

export class ResourceController {
@Method.Get('resource/:resourceId', {
permissions: ['resource.get'], // extension
responses: t(Ok.Type(ResourceDto), NotFound.Type(NotFoundDto)),
})
// You can remove return type annotation and still have type safety
async getResource(): Promise<Ok<ResourceDto> | NotFound<NotFoundDto>> {
return Ok(new ResourceDto('id', 'name'));
}
}

Specify endpoint decorator logic:

// Source: overrides.ts

import { setEndpointDecorator } from 'typed-http-decorators';

setEndpointDecorator((method, path, { permissions }) => (cls, endpointName) => {
// Your endpoint decoration logic:
console.log(
`Decorating ${cls.name}.${String(endpointName)}`,
`with route ${method} /${path} with permissions: ${permissions}`
);
});

Customization

You can extend EndpointOptions like this:

declare module 'typed-http-decorators' {
interface EndpointOptions {
permissions: string[];
}
}

You can completely change HttpResponseOptions by setting the override field like this:

declare module 'typed-http-decorators' {
interface HttpResponseOptionsOverrides {
override: {
description?: string;
};
}
}

The original HttpResponseOptions is available with HttpResponseOptionsOverrides["default"]

You can also override HttpResponseTypeOptions in the same way.

Nest.js integration

Controller example:

// Source: src/films/films.controller.ts

import { TypedResponseInterceptor } from '../common/typed-response.interceptor';
import { Controller, UseInterceptors } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Method, Ok, t } from 'typed-http-decorators';
import { FilmsDto } from './dto/films.dto';
import { FilmsService } from './films.service';

@ApiTags('films')
@Controller('films')
/* You need to transform typed responses to the ones accepted by Nest.js */
@UseInterceptors(new TypedResponseInterceptor())
export class FilmsController {
constructor(private films: FilmsService) {}

@Method.Get('', {
summary: 'Get all films', // extension
description: 'Gets all films from the database', // extension
responses: t(
Ok.Type(FilmsDto, {
description: 'A list of films', // extension
})
),
})
async getAllFilms() {
return Ok(
new FilmsDto({
films: [new FilmDto('id', 'name'), new FilmDto('id', 'name')],
})
);
}
}

Decorator logic:

// Source: src/common/http-decorators-logic.ts

import { applyDecorators, RequestMapping, RequestMethod } from '@nestjs/common';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { setEndpointDecorator } from 'typed-http-decorators';

declare module 'typed-http-decorators' {
interface EndpointOptions {
/* This way you force endpoint options to be specified */
summary: string;
/* Or you can also make them optional */
description?: string;
}

interface HttpResponseOptionsOverrides {
override: {
description?: string;
};
}
}

setEndpointDecorator((method, path, { responses, summary, description }) =>
applyDecorators(
// apply Nest.js specific endpoint decorators
RequestMapping({ method: RequestMethod[method], path }),
...responses.map(({ status, body, options }) =>
ApiResponse({ status, type: body, description: options?.description })
),

// extensions are also available
ApiOperation({ summary, description })
)
);

Typed responses interceptor:

// Source: src/common/typed-response.interceptor.ts

import {
BadRequestException,
CallHandler,
ExecutionContext,
Injectable,
InternalServerErrorException,
NestInterceptor,
} from '@nestjs/common';
import { catchError, map, throwError } from 'rxjs';

@Injectable()
export class TypedResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
map(({ status, body }) => {
context.switchToHttp().getResponse().status(status);
return body;
}),
catchError((error) =>
throwError(() =>
!(error instanceof BadRequestException) ? new InternalServerErrorException() : error
)
)
);
}
}

Entrypoint:

// Source: src/main.ts

/*
!!! Remember to have decorator logic as the first import !!!
*/
import './common/http-decorators-logic';
import { NestFactory } from '@nestjs/core';
import { AppModule } from '@/app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

Note: If all your endpoints return typed responses you can apply TypedResponseInterceptor globally instead of applying it to each controller:

app.useGlobalInterceptors(new TypedResponseInterceptor());

Testing

When testing your controllers you must also import your decorator logic before applying typed-http-decorators.

You can do this automatically with Jest:

{
// jest configuration ...
"setupFiles": [
"./src/common/http-decorators-logic.ts" // your decorator logic
// other setup files ...
]
}

If your testing framework does not support this feature you can manually import your decorator logic in the first import of each testing module.

Bootstrapped with: create-ts-lib-gh

This project is Mit Licensed.

Generated using TypeDoc