1.6.1 • Published 2 years ago

typed-http-decorators v1.6.1

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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.

1.6.1

2 years ago

1.6.0

2 years ago

1.5.0

2 years ago

1.4.0

2 years ago

1.3.0

2 years ago

1.2.0

2 years ago

1.1.2

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago