open-api

repository·main·Indexed 21 days ago

https://github.com/kogosoftwarellc/open-api

A monorepo of packages providing OpenAPI support for Node.js applications, including validation, coercion, and framework integrations for Express and Koa. It features express-openapi, which automates path registration, dependency injection, and Swagger UI setup based on OpenAPI 3.0 or Swagger 2.0 specifications.

Tokens
28.4K
Snippets
64
Records
103
Agent score
72%

What's inside open-api

  1. Overview of @open-api packages

    main

    The @open-api monorepo provides a suite of packages designed to power OpenAPI implementations in Node.js environments. The available packages include:

    • express-openapi: Integration for Express.
    • koa-openapi: Integration for Koa.
    • fetch-openapi: OpenAPI support for fetch.
    • fs-routes: File-system based routing.
    • openapi-default-setter: Sets default values based on OpenAPI specs.
    • openapi-framework: Core framework logic.
    • openapi-jsonschema-parameters: Handles JSON Schema parameters.
    • openapi-request-coercer: Coerces incoming requests to match OpenAPI types.
    • openapi-request-validator: Validates incoming requests against OpenAPI specs.
    • openapi-response-validator: Validates outgoing responses against OpenAPI specs.
    • openapi-schema-validator: Validates JSON schemas.
    • openapi-types: TypeScript definitions and types.
  2. Set default values in request properties with openapi-default-setter

    main

    Use openapi-default-setter to automatically populate missing header and query request properties with default values defined in your OpenAPI (Swagger 2.0) parameter lists.

    Key Behaviors:

    • Supported Locations: It sets defaults for header and query parameters.
    • Unsupported Locations: Path parameters are not supported (it is assumed path parameters will always have a value).
    • No Coercion or Validation: This package does not perform type coercion or request validation. For those tasks, use openapi-request-coercer and openapi-request-validator respectively.
    import OpenAPIDefaultSetter from 'openapi-default-setter';
    
    const defaultSetter = new OpenAPIDefaultSetter({
      parameters: [
        {
          in: 'query',
          type: 'integer',
          name: 'foo',
          default: 5
        }
      ]
    });
    
    const request = { query: {} };
    
    defaultSetter.handle(request);
    
    console.log(request.query.foo); //=> 5
  3. Understand the role of openapi-framework

    main

    The openapi-framework package serves as a foundational, framework-agnostic OpenAPI engine. It is designed to provide the core logic required to implement OpenAPI capabilities (like validation and routing) across any web framework. Instead of using it directly for a specific server, you typically use one of its framework-specific implementations:

    • express-openapi: For Express.js applications.
    • koa-openapi: For Koa.js applications.

    If you are looking for implementation examples, you can refer to the sample projects in the repository's test directory.

  4. Use promiseMode for async/await handlers

    main

    By default, middleware and path handlers are expected to follow the standard Express callback pattern (req, res, next). If you set args.promiseMode: true, you can return Promises or use async/await in your handlers.

    // With promiseMode: true
    async function GET(req, res) {
      const worlds = await worldsService.getWorlds(req.query.worldName);
      if (!worlds.length) {
        throw { status: 404, message: 'No worlds were found' };
      }
      res.status(200).json(worlds);
    }
  5. Understand the output format of openapi-jsonschema-parameters

    main

    The convertParametersToJSONSchema function produces an object where keys represent the parameter location (in).

    • body: Returns the schema directly.
    • headers, path, and query: Returns an object containing a properties map of the parameters, and for query, a required array listing the names of required parameters.

    Example output structure based on the conversion of body, header, path, and query parameters:

    {
      "body": {
        "$ref": "#/definitions/SomeDefinition"
      },
      "headers": {
        "properties": {
          "Accept": {
            "type": "string"
          }
        }
      },
      "path": {
        "properties": {
          "boo": {
            "type": "string"
          }
        }
      },
      "query": {
        "properties": {
          "foo": {
            "type": "string"
          }
        },
        "required": ["foo"]
      }
    }
  6. Inject dependencies into path handlers

    main

    If your route files export a function instead of an object, you can use args.dependencies to inject services or data providers directly into the function arguments. The keys in the dependencies object must match the parameter names in your exported function.

    Implementation Pattern

    1. Define dependencies in the initialize call.
    2. Export a function from your route file that accepts those dependencies as arguments.
    // app.js
    var mockDataProvider = require("custom-mock-data-provider");
    var geoService = require("awesome-geo-service")({url: "http.example.com/geoservice"});
    
    initialize({
        apiDoc: require('./api-doc.js'),
        app: app,
        paths: [path.resolve(__dirname, 'api-paths')],
        dependencies: {
            dataprovider: mockDataProvider(),
            geoservice: geoService
        }
    });
    
    // api-paths/users.js
    module.exports = function(geoservice, dataprovider) {
        var doc = {
            GET: function (req, res, next) {
                res.json({
                    user: dataprovider.getUser(req.params.userid), 
                    location: geoservice.getUserLocation(req.params.userid)
                });
            }
        };
        doc.GET.apiDoc = { /* ... */ };
        return doc;
    };
  7. Use openapi-request-validator to validate requests

    main

    Use openapi-request-validator to validate incoming request properties (headers, body, params, query) against an OpenAPI specification.

    Key Behaviors:

    • It leverages jsonschema for validation.
    • It supports $ref in body schemas (e.g., #/definitions/SomeType).
    • Note: It does not perform type coercion (use openapi-request-coercer) or supply default values (use openapi-default-setter).
    • Note: It does not validate parameter input directly; it converts parameter input to JSON Schema using openapi-jsonschema-parameters.

    To use it, instantiate OpenAPIRequestValidator with your specification details and call validateRequest(request).

    var OpenAPIRequestValidator = require('openapi-request-validator').default;
    
    var requestValidator = new OpenAPIRequestValidator({
      parameters: [
        {
          in: 'query',
          type: 'string',
          name: 'foo',
          required: true
        }
      ],
      requestBody: {
        content: {
          'application/json': {
            schema: {
              properties: {
                name: { type: 'string' }
              }
            }
          }
        }
      },
      schemas: null,
      errorTransformer: null,
      customFormats: {
        foo: function(input) {
          return input === 'foo';
        }
      }
    });
    
    var request = {
      headers: { 'content-type': 'application/json' },
      body: {},
      params: {},
      query: {foo: 'wow'}
    };
    
    var errors = requestValidator.validateRequest(request);
    if (errors) {
      console.log(errors);
    }
  8. Coerce request properties with OpenapiRequestCoercer

    main

    Use OpenapiRequestCoercer to transform request properties (header, path, query, and formData) into the types defined in an OpenAPI parameters list. It handles array types and supports _collectionFormat_ for formData array parameters.

    To use it, instantiate the class with a parameters array and call .coerce(request) on your request object. The coercion happens in-place on the provided request object.

    import OpenapiRequestCoercer from 'openapi-request-coercer';
    
    const coercer = new OpenapiRequestCoercer({
      parameters: [
        {
          in: 'query',
          type: 'integer',
          name: 'foo',
          required: true
        }
      ]
    });
    
    const request = {
      query: {
        foo: '5'
      }
    };
    
    coercer.coerce(request);
    console.log(request.query.foo); //=> 5
  9. Implement custom security handlers

    main

    To support security schemes defined in your apiDoc (like apiKey or basic auth), provide a mapping of scheme names to handler functions in args.securityHandlers. Each handler is responsible for validating the request and can return a Promise.

    Configuration

    1. Define securityDefinitions in your apiDoc.
    2. Map those names to handlers in initialize.
    3. Use the security property in your operation apiDoc to apply them.
    // 1. apiDoc definition
    var apiDoc = {
      swagger: 2.0,
      securityDefinitions: {
        keyScheme: {
          type: 'apiKey',
          name: 'api_key',
          in: 'header'
        }
      }
    };
    
    // 2. Initialization
    initialize({
      apiDoc: apiDoc,
      app: app,
      securityHandlers: {
        keyScheme: function(req, scopes, definition) {
          // Validate key and return Promise.resolve(true) or reject
          return Promise.resolve(true);
        }
      }
    });
    
    // 3. Operation usage
    function myHandler(req, res) { /* ... */ }
    myHandler.apiDoc = {
      security: [{ keyScheme: [] }]
    };
  10. Quick Start with express-openapi

    main

    To use express-openapi, follow these four steps to set up a documented API with automatic path generation and dependency injection:

    1. Create an apiDoc: Define your main OpenAPI specification (Swagger 2.0 or OpenAPI 3.0) in a JavaScript object or a YAML file. You can leave the paths object empty, as express-openapi will populate it based on your path handlers.
    2. Create path handlers: Place your path handlers in a directory (e.g., ./api-v1/paths/). Each file should export an object where keys are HTTP methods (e.g., GET, POST). To enable OpenAPI features like validation for a specific method, attach an apiDoc property to that method function.
    3. Create services: Define your business logic in service files. These can be injected into your path handlers.
    4. Initialize the app: Use the initialize function, passing your Express app, the apiDoc, the directory containing your paths, and a dependencies object to map service names to their implementations.

    express-openapi automatically adds a Swagger UI route at apiDoc.basePath + args.docsPath.

    // 1. api-doc.js
    const apiDoc = {
      swagger: '2.0',
      basePath: '/v1',
      info: { title: 'API', version: '1.0.0' },
      definitions: { ... },
      paths: {}
    };
    
    // 2. paths/worlds.js
    export default function(worldsService) {
      let operations = { GET };
      function GET(req, res, next) {
        res.status(200).json(worldsService.getWorlds(req.query.worldName));
      }
      GET.apiDoc = {
        summary: 'Returns worlds by name.',
        parameters: [{ in: 'query', name: 'worldName', required: true, type: 'string' }],
        responses: { 200: { description: 'Success', schema: { type: 'array', items: { $ref: '#/definitions/World' } } } }
      };
      return operations;
    }
    
    // 3. services/worldsService.js
    const worldsService = { getWorlds: (name) => [...] };
    
    // 4. app.js
    import { initialize } from 'express-openapi';
    const app = express();
    initialize({
      app,
      apiDoc: v1ApiDoc,
      dependencies: { worldsService: v1WorldsService },
      paths: './api-v1/paths'
    });
    app.listen(3000);
  11. Scan a filesystem for routes with fs-routes

    main

    Use fs-routes to scan a directory and generate a list of routes based on the directory structure. This is framework-agnostic and can be used to load route modules for any Node.js web framework. It supports convention-based routing, including parameters (e.g., :id) and index files.

    Each result in the output array contains:

    • path: The absolute filesystem path to the route file.
    • route: The generated URL path string.
    import fsRoutes, { FsRoute } from 'fs-routes';
    
    // Pass the directory to scan
    const output: FsRoute[] = fsRoutes('routes');