0.6.5 • Published 6 months ago

openapi-routing v0.6.5

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

API-first REST microservice from OpenAPI schema

The openapi-routing library is a minimalistic solution to create a microservice from an OpenAPI schema.

OpenAPI 3 is de-facto standard for defining interfaces of REST API (micro)services.

Single source of truth

This library makes it really simple to implement a microservice "the right way" - starting from definition of microservice interface - the contract to client applications, and implementing the request handlers in directory and file structure that is determined by paths defined in the interface - in OpenAPI schema.

OpenAPI schema is treated as ultimate single source of truth by describing and declaring interface / contract of our microservice. From this contract are derived all the routing rules. The routing library calls appropriate handling functions according the routing rules. Handler functions are implementations of microservice behavior / functionality, they take request parameters and respond with resulting data. The response with headers and data is composited in the routing library.

The openapi-routing library serves as a lightweight routing specified by Open API 3 either without any framework - as vanilla Node.js application, or as a middleware in Express application.

A microservice with openapi-routing is written in 2 steps:

  1. Designing the REST API interface - an OpenAPI schema
  2. Writing handler functions - modules with names defined by paths in the OpenAPI schema

Let's try it out

To have an idea what the library does and how is the application logic for API server implemented, you may clone the project and use an example server that shows an API built form OpenAPI schema.

  1. Clone openapi-routing to your local machine with command
    git clone https://github.com/tomi-vanek/openapi-routing.git
  2. Load dependencies with command
    npm install
  3. Run the example server with command
    npm start
  4. Test in browser with following URLs:

    http://localhost:8080/v1/artists/Wolfgang%20von%20Kempelen
    http://localhost:8080/v1/artists?limit=321&offset=32
    http://localhost:8080/v1/stats
    http://localhost:8080/meta/health

Create your own REST API microservice

  1. Design a new schema for the new REST API / microservice (for exploration purposes you may copy the example-server/simple-api.yaml schema). Test the syntax of your schema with online OpenAPI validator.
  2. Create a directory for your new REST microservice and go to that directory
    mkdir <rest-project> && cd  <rest-project>
  3. Initialize the microservice project and create the project configuration file package.json with command:
    npm init
  4. Add to the package.json configuration file following line to switch module loading to standard EcmaScript module system:
    "type": "module"
  5. Install and load the openapi-routing library with command
    npm install openapi-routing
  6. Create a directory for request handlers (by default I use handlers name) and handler modules. If you want just try out the library, you can copy the directory example-server/handlers into your project.
    mkdir handlers
  7. Create API server start module server.js.
    • Node.JS: If you want just try out the library, you may take the file example-server/server.js and change the import line:
      import { routerForSchema, readSchema, endpointsMessage } from 'openapi-routing';
    • Express: Alternative option - if you want to use express with usefull middlewares, you can take the file example-server/express-server.js as your starting point and make similar change as in previous step. Express library is optional dependency - it is not istalled by default. If you want to test the express-server.js, you have to force-install with command npm i -f

How it works

The openapi-routing library can be used in plain Node.js http / https createServer - no need to include additional framework (i.e. Express) to run a lightweight API server.

Server start

  1. By server start the openapi-routing reads Open API 3 schema for the REST service, either in YAML or in JSON format.
  2. The openapi-routing identifies the paths from schema, and for each path the methods served in that path.
  3. For each path and method is created a routing rule. Path is mapped to a JavaScript path and file in <project-root-dir>/handlers directory. Each HTTP method for the path in schema has a handler function exported in the ES6 handler module.

HTTP request handling

  1. By each HTTP request from a client, the openapi-routing finds the routing rule for incoming URL path and HTTP method.
  2. The openapi-routing reads From the routing rule the file path to the handler module, and the name of the handler function in the module.
  3. The openapi-routing opens the handler module, and calls asynchronously the handler function with incomming parameters and if appropriate, also with data from request body (i.e. by POST request). After the handler function finishes the processing, the return value is sent as HTTP response to the calling client.

Handlers - application logic

Application logic is organized into handler modules and implemented in handler functions.

Handler modules export handler functions that are called by routing logic to process API requests.

Handler modules are by convention in directory <project-root-dir>/handlers. Each handler module path and name is identical with the http or https path in OpenApi schema that receives .js extension.

Example:

We use the example server's schema and folder structure for request handlers

  • The path /artists has handler module <project-root-dir>/handlers/artists.js.
  • The path /artists/{username} has handler module <project-root-dir>/handlers/artists/{username}.js ... curly brackets are valid characters in file name ;-).

In example server we have in schema these paths:

paths:
  /artists:
    get:
        # ...
    post:
        # ...

  /artists/{username}:
    get:
        # ...

  /stats:
    get:
        # ...

For the 3 paths we have following directory structure for request handlers:

handlers
  +- artists
  |    +- {username}.js
  +- artists.js
  +- stats.js

Each handler module serves specific path defined in the OpenAPI schema. Handler module is a plain simple ES6 module (defined in JavaScript standards and supported in current Node.js).

For each HTTP method specified in the OpenAPI schema, the handler module exports a handler function. Name of the handler function is by convention handle<METHOD> - i.e. handleGet(parameters), handlePost(parameters, data). HTTP methods are GET, POST, PUT, PATCH, DELETE, HEAD, etc.

Router reads parameters from URL path and from query. Parameters are merged to parameters object. If the request provides also data in body (in JSON format), the router reads incomming data, concatenates them (supporting multipart - data in more calls from client), and provides these parameters and data to handler function.

Handler function is asynchronous - it is defined as async, so if processing is long, it does not block the server by executing handlers for concurrent requests in parallel. Handler function returns data that will be sent to client either directly in text format or and object that is going to be serialized to JSON format, or data in binary format together with the mime format definition.

Binary return value is an object containing:

  • mime --> MIME type (IANA media type), i.e. "image/png"
  • data --> buffer or stream with binary data
  • (optional) fileName --> if defined, the file is provided as download

All the necessary headers for response are set by the routing library.

Example of a handler with simple return value - a JavaScript object that will be serialized by the routing library for HTTP response into JSON format:

export async function handleGet(params) {
  // parameter handling, create response data
  // ... in this case for test we return params without any change
  return params;
}

export async function handlePost(params, data) {
  // parameter handling, create response data
  // ... in this case for test we return input data without any change
  return data;
}

Example of a handler with binary return value that can be rendered in browser (i.e. an image):

import {promises as fsPromises} from 'fs';
import path from 'path';

const fileName = 'pie-chart.jpg';
const fileNameWithPath = path.normalize(
    new URL('.', import.meta.url).pathname + '../assets/' + fileName
);

export async function handleGet() {
    return {
        mime: 'image/jpg',
        data: fsPromises.readFile(fileNameWithPath),
    };
}

Example of a handler with binary return value, when the browser should download it - not open it (i.e. a zip file). In this case we provide also file name. The file name in response object is a sign for the routing library to add header parameters to signalize that the response should be treated as file download:

import {promises as fsPromises} from 'fs';

import {promises as fsPromises} from 'fs';
import path from 'path';

const fileName = 'pie-chart.jpg';
const fileNameWithPath = path.normalize(
    new URL('.', import.meta.url).pathname + '../assets/' + fileName
);

export async function handleGet() {
    return {
        mime: 'image/jpg',
        data: fsPromises.readFile(fileNameWithPath),
        fileName,
    };
}

Exceptions in handlers

Default HTTP response code by an exception thrown by handler is 500 = internal server error. If the handler wants to decide about returned status code - i.e. 404 (Not found) or 400 (Bad input) - the thrown error code must contain integer value in property status.

    const errors = validate(parameters);
    if (errors) {
        const err = new Error(errors);
        err.status = 400;
        throw err;
    }

The responses.js module provides a convenient error class with status - ErrorWithStatus. An example with error that determines the HTTP response status code:

    const errors = validate(parameters);
    if (errors) {
        throw new ErrorWithStatus(errors, 400);
    }

Meta endpoints

The routing library offers also endpoints that provide "technical" meta information about the API. These endpoints are not declared in the schema and should make the microservice easily deployable to production environment:

  • /meta/schema.yaml - provides the API schema in YAML format
  • /meta/schema.json - provides the API schema in JSON format
  • /meta/health - health check for simpler deployment to cloud environment
  • /meta/info - basic information about the service - extracted from the OpenAPI schema
  • /meta/routing - routing rules used for request handling (for development and debugging)

User interface

Very useful fearure for a microservice is web user interface generated form the OpenAPI schema. User interface can serve as human-readable documentation, as experimenting tool for developers or as a tool for testers.

Adding UI to your microservice is simple: 1. Create directory with name ui in root of the microservice 1. Download the newest release of Swagger UI from page https://github.com/swagger-api/swagger-ui/releases 1. Unzip the code, and copy hte content of the directory release into the directory ui in your project 1. Edit the file /ui/index.html:

  • Change the schema URL to "/meta/schema.yaml":

    const ui = SwaggerUIBundle({
      url: "/meta/schema.yaml",
      //...
    });
  • Optional: change the configuration of the UI application. I usually add following lines to the SwaggerUIBundle configuration object:

    const ui = SwaggerUIBundle({
        //...
    
        // custom configuration:
        tryItOutEnabled: true,
        jsonEditor: true,
        showRequestHeaders: true,
        defaultModelExpandDepth: 5,
        defaultModelsExpandDepth: 5,
        defaultModelRendering: "model",
        displayRequestDuration: true,
        docExpansion: "list",
    });
  • Optional: change the look & feel of the application - select a theme from https://ostranme.github.io/swagger-ui-themes/

  • Optional: change the logo, favicon, title in HTML, change fonts and colors with CSS, maybe remove the title bar ... The visual aesthetic is very important - the generated UI is "face" of your API microservice for developers and testers. The time spent in this refinement is worth it.

Roadmap

Ideas for enhancements in future releases of openapi-routing:

  • Configuration
  • Plugable architecture for simple integration and for extensions --> processing cascade
  • Headers for secure responses
  • Support for compressed responses
  • Code generator for initial structure of handlers and Dockerfile for simple deployment

Credits

0.6.5

6 months ago

0.6.4

6 months ago

0.6.3

8 months ago

0.6.2

1 year ago

0.6.1

1 year ago

0.6.0

1 year ago

0.5.7

1 year ago

0.4.28

1 year ago

0.4.29

1 year ago

0.4.32

1 year ago

0.4.30

1 year ago

0.4.33

1 year ago

0.5.4

1 year ago

0.5.3

1 year ago

0.5.5

1 year ago

0.5.0

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.4.20

2 years ago

0.4.21

1 year ago

0.4.26

1 year ago

0.4.27

1 year ago

0.4.24

1 year ago

0.4.22

1 year ago

0.4.23

1 year ago

0.4.19

2 years ago

0.4.17

2 years ago

0.4.18

2 years ago

0.4.16

2 years ago

0.4.10

2 years ago

0.4.9

2 years ago

0.4.8

2 years ago

0.4.15

2 years ago

0.4.14

2 years ago

0.4.11

2 years ago

0.4.12

2 years ago

0.4.7

2 years ago

0.4.6

2 years ago

0.3.0

2 years ago

0.4.5

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.4.3

2 years ago

0.2.0

2 years ago

0.1.0

2 years ago