openapi-backend icon indicating copy to clipboard operation
openapi-backend copied to clipboard

API Versioning / Multiple OpenAPI Backends: potential issue?

Open germatpreis opened this issue 3 years ago • 0 comments

Hi there,

I need to support multiple versions of my API and checked the official documentation and also the referenced example at etherpad-lite. I am using version 5.3.0 of openapi-backend.

However, when wiring the api.handleRequest calls into express

app.use(apiRoot, async (req, res) => {
  return api.handleRequest(req as OpenApiRequest, req, res);
});

neither of the versioned APIs gets called and I'll get a Error: 404-notFound: no route matches request. I dug around a bit and found an a existing (closed) issue which very much described the issue that I am having.

I can't use your solution #1 since multiple apiRoots is basically what I need. So I tried your solution #2

app.use(apiRoot, async (req, res) => {
  return api.handleRequest({ ...req, path: req.baseUrl + req.path } as OpenApiRequest, req, res);
});

and this works, the requests are now correctly routed and arrive at the endpoints.

Atm, I don't know if this is a bug, or 'works as designed'. If its the latter, maybe it would make sense to add this to the documentation or create an example for supporting multiple versions in the repo (I could create a PR for this).

For reference, here is the code that I used for testing:

// index.ts
import express, { Request, Response } from 'express';
import { Context, OpenAPIBackend, Request as OpenApiRequest } from 'openapi-backend';
import path from 'path';

async function run() {

  const app = express();
  app.use(express.json());

  const port = 3000;

  let versions: Record<string, string> = {
    'v1': 'I am old version 1',
    'v2': 'I am new version 2'
  };

  for (const version of Object.keys(versions)) {
    let apiRoot = `/${version}`;

    let api = new OpenAPIBackend({ definition: path.join(__dirname, `./${version}.yaml`), apiRoot: apiRoot });

    api.register({
      getVersion: (c: Context, req: Request, res: Response) => {

        const content = versions[version];
        
        console.log(`Serving content for ${apiRoot}: ${content}`);
        console.log(`Available query params: ${JSON.stringify(c.request.query)}`);

        return res.send(content);
      },
      createVersion: (c: Context, req: Request, res: Response) => {
        console.log(`Received CreateVersionRequest for version ${c.request.body.newVersion}`);
        return res.status(200).send();
      }
    });

    api.register('validationFail', (c: Context, req: Request, res: Response) => {
      return res.status(400).json({ message: 'Invalid client request', details: c.validation.errors?.map(e => e.message) });
    });
    api.register('notImplemented', (c: Context, req: Request, res: Response) => {
      res.status(501).json({ message: 'Not Implemented' });
    });
    api.register('notFound', (c: Context, req: Request, res: Response) => {
      return res.status(404).json({ message: 'Not found' });
    });

    await api.init();

    app.use(apiRoot, async (req, res) => {
      // bug still at large? https://github.com/anttiviljami/openapi-backend/issues/81
      // THIS WORKS:
      return api.handleRequest({ ...req, path: req.baseUrl + req.path } as OpenApiRequest, req, res);
      // THIS DOESN'T:
      // return api.handleRequest(req as OpenApiRequest, req, res);
    });
  }

  app.listen(port, () => {
    console.log(`Server is running at https://localhost:${port}`);
  });

}

run();
# v1.yaml
openapi: 3.0.1
info:
  title: A Component (Version 1.0.0)
  version: 1.0.0
servers:
  - url: http://localhost:3000/v1
paths:
  /version:
    get:
      operationId: getVersion
      parameters:
        - name: testParamOnlyInV1
          required: true
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: 'ok'
          content:
            application/text:
              schema: 
                type: string
# v2.yaml
openapi: 3.0.1
info:
  title: A Component (Version 2.0.0)
  version: 2.0.0
servers:
  - url: http://localhost:3000/v2
paths:
  /version:
    get:
      operationId: getVersion
      parameters:
        - name: testParamOnlyInV2
          required: true
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: 'ok'
          content:
            application/text:
              schema: 
                type: string
    post:
      operationId: createVersion
      requestBody:
        required: true
        content:
          application/json:
            schema: 
              $ref: '#/components/schemas/CreateVersionRequest'
      responses:
        '200':
          description: OK
components:
  schemas:
    CreateVersionRequest:
      required:
        - newVersion
      properties:
        newVersion:
          type: string

germatpreis avatar Aug 07 '22 13:08 germatpreis