/openapi-ts-backend

Simple backend implementations of OpenAPI specifications with full TypeScript typing of schemas and operations and couplings for AWS Lambda

Primary LanguageTypeScriptMIT LicenseMIT

@openapi-ts/backend

Enables easy implementions of OpenAPI REST APIs in TypeScript with full typings of schemas and operations.

NOTE: This is version 2 of the openapi-ts-backend module, in which the single module from v1 was restructured into several modules, and service integrations for AWS Lambda and Express moved to separate modules. Version 2 is not backwards compatible with version 1, which is available at https://www.npmjs.com/package/openapi-ts-backend

Introduction

This module allows for simple REST API implementation in TypeScript using OpenAPI 3.0 specifications. It can be easily integrated with any HTTP framework such as Express, AWS Lambda etc. Connectors for AWS Lambda and Express are provided as add-on modules.

The module uses the excellent https://www.npmjs.com/package/openapi-backend module for routing and validation, and adds some useful features on top:

  • Executable for generating TypeScript types for all schemas and operations. This is built on top of https://www.npmjs.com/package/openapi-typescript, and adds full types for all the operations specified in the API.
  • Typed requests, responses etc, with headers and other parameters being coerced to fit the API schemas.
  • Interceptors ("middleware") support
  • Support for multiple OpenAPI specifications, mounted at different root paths.
  • Simplified authorization
  • Customizable response validation and trimming
  • Simple error handling and customizable error response formatting

Installation

$ npm install @openapi-ts/backend

Note however that the service add-ons exports all members from this module, so if you want to use the bindings for e.g. AWS Lambda or Express, you only have to install a single module:

For AWS Lambda:

$ npm install @openapi-ts/aws-lambda

For Express:

$ npm install @openapi-ts/express

Usage

Simple Hello World API example:

This example creates an API without service bindings to AWS Lambda or other services:

Create API:

const api = new OpenApi()
  .register({
    definition: './greet-api.yml', // JSON or YAML
    operations: {
      // map of specification operationIds to handler functions
      greet: req => {
        return `Hello, ${req.params.name}!`;
      }, 
      ... 
    }
  });

Invoke API:

const res = await api.handleRequest({
  method: 'GET',
  path: '/greet/world',
  headers: {},
});

Generating types for APIs

Consider this example API:

greet-api.yml

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Greet API
components:
  securitySchemes:
    AccessToken:
      type: oauth2
      description: 'Validates a bearer token'
      flows:
        password:
          tokenUrl: 'https://api.example.com/oauth/token'
          scopes:
            full: Full access
            some: Some access
  schemas:
    Title:
      type: string
      enum:
        - Mr
        - Mrs
        - Miss
    Person:
      type: object
      description: A person
      required:
        - name
      properties:
        name:
          type: string
        title:
          $ref: '#/components/schemas/Title'
        photo:
          type: string
          format: byte
    Greeting:
      type: object
      required:
        - message
      properties:
        message:
          type: string
paths:
  /greet/{name}:
    get:
      operationId: greet
      summary: Greet the caller
      description: This greets the caller
      security:
        - AccessToken:
            - some
            - full
      parameters:
        - in: path
          name: name
          schema:
            type: string
          required: true
        - in: query
          name: title
          schema:
            $ref: '#/components/schemas/Title'
          description: Bar
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Greeting'

To generate TypeScript types for all requests and operation handlers, an executable is provided:

$ npx openapi-ts-backend generate-types greet-api.yml src/gen/greet-api
...
Using TypeScript v4.2.3
Types written to src/gen/greet-api

Now, types for all schemas and operations are generated in src/gen/greet-api, and a backend service can be set up type safely.

Note 1: Typically it's convenient to add type generation as part of your tool chain for building and/or deploying your code, and not put the generated types into source control. However, if you do want to use the generated request types stand-alone without any bindings to operation handlers used by the OpenApi class, it's possible by also installing the core requests types used by the generated code using npm install @openapi-ts/request-types and invoking the code generator with the --exclude-handlers option.

Note 2: The code generator uses typescript as a peer dependency so make sure it's available as a devDependency from your module or as a globally installed module.

Example AWS Lambda API with typed operations

src/api.ts

import {LambdaOpenApi} from '@openapi-ts/aws-lambda';
import * as GreetApi from './gen/greet-api';

const operations: GreetApi.OperationHandlers<LambdaSource> = {
  greet: (req, res, params) => {
    const {params: {name}, query: {title}} = req; 

    // All request data is typed:
    // * req.params is {name: string}
    // * req.query is {title?: 'Mr' | 'Mrs' | 'Miss'}.
    // * Response body is {message: string} | void
    //   (all handlers may return void and mutate res instead)

    return {
      message: `Hello, ${title ? title + ' ' : ''}${name}!`;
    }
  }
};

const api = new LambdaOpenApi()
    .intercept(((req, res, params) => {
      console.log(`Event:`, params.data.lambda.event);
    }))
    .register({
      definition: './greet-api.yml',
      operations,
      path: '/'
    });
    
export default api.eventHandler();

The OperationHandlers interface contains all the operations specified by the OpenAPI specification, each one with typings for headers, path params, query params, cookies, bodies and responses, making it really easy to implement the API using TypeScript.

For simplicity, each operation may return a response body, or the provided res object may be mutated instead. If the response object is not mutated and a body is returned, the response will use the returned value as the body. The response status code is set by modifying res.statusCode, however as a convenience it may be omitted if the operation only has a single defined successful status code. If statusCode is not set and there are multiple successful status codes defined for the operation, a HTTP 500 error will be thrown.

Requests are always validated, and headers, path parameters, query parameters and cookies are parsed an coerced to the operation schema. If a request does not match the operation schema, a HTTP 400 response is returned.

Responses are by default validated with errors simply logged. By using responseValidationStrategy: 'throw', invalid responses will instead render HTTP 500 errors.

All error responses are customizable by supplying a custom errorHandler which maps thrown Error objects to HTTP responses.

Each handler is invoked with a params object containing API data such as the operation, authorizations etc. Custom data can be provided in params by supplying them to handleRequest and by specifying a custom type parameter to the OpenApi constructor:

const api = new OpenApi<MyCustomData>().register(...);
api.handleRequest(req, myCustomData);

The myCustomData object will now be present in each handler as params.data.

Interceptors

Interceptors are functions invoked on every request, similar to Express middleware. Note that interceptors are invoked before requests are parsed and routed.

Authorizers

Authorizers are functions implementing a security scheme as defined in the OpenAPI specification. An authorizer function is called with the parsed request as input together with the scopes required for the current operation (if applicable to the security scheme). The authorizer should either return some data, such as a session or user object, or throw an error. If multiple security requirements are provided for an operation, only one must be successful.