A couple of weeks ago I finally had the time to start experimenting with NodeJS API building and after some research decided to go with a combination of Express + Sequelize + PostgresSql + OpenAPI. At this time I skipped using TypeScript and opted for going with JS ES6+. After reading official documentation for the OpenAPI Specification and the npm package I'm using (express-openapi), I've been going back and forward from openapi 3.x to swagger 2.0 and switching the api-doc from JSON to yml but in the end I always get stuck at the same point with this error:
SyntaxError: Invalid regular expression: /^\/v2\/users\(?:([^\/]+?))\/?$/i: Unmatched ')'
at new RegExp (<anonymous>)
at pathtoRegexp (...\express-sequelize-openapi\node_modules\path-to-regexp\index.js:128:10)
at new Layer (...\express-sequelize-openapi\node_modules\express\lib\router\layer.js:45:17)
at Function.route (...\express-sequelize-openapi\node_modules\express\lib\router\index.js:505:15)
at app.<computed> [as get] (...\express-sequelize-openapi\node_modules\express\lib\application.js:498:30)
at Object.visitOperation (...\express-sequelize-openapi\node_modules\express-openapi\dist\index.js:131:33)
at ...\express-sequelize-openapi\node_modules\openapi-framework\dist\index.js:370:29
at Set.forEach (<anonymous>)
at ...\express-sequelize-openapi\node_modules\openapi-framework\dist\index.js:233:100
at Array.forEach (<anonymous>)
These are some of the dependencies I'm using with the current versions on my package.json:
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.19.1",
"express-openapi": "^12.1.3",
"helmet": "^7.1.0",
"pg": "^8.11.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.1",
"sequelize-cli": "^6.6.2",
"swagger-ui-express": "^5.0.0",
Then this is the content of app.js:
import express from "express";
import bodyParser from "body-parser";
import cors from "cors";
import helmet from "helmet";
import path from "path";
import swaggerUi from "swagger-ui-express";
import { initialize as initializeOpenApi } from "express-openapi";
import { fileURLToPath } from "url";
import v1ApiDoc from "./doc/api-doc-v2.js";
// Defining the Express app
const app = express();
// Adding Helmet to enhance your Rest API's security
app.use(helmet());
// Using bodyParser to parse JSON bodies into JS objects
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Enabling CORS for all requests
app.use(cors());
const docsPath = "/openapi.json";
// OpenAPI UI
app.use("/api-documentation", swaggerUi.serve, swaggerUi.setup(v1ApiDoc));
export const startApp = async (port) => {
// OpenAPI routes
const openApiDoc = await initializeOpenApi({
apiDoc: v1ApiDoc,
app,
docsPath,
paths: "./api/routes/v2",
});
app.use(function (err, req, res, next) {
res.status(err.status).json(err);
});
// Starting the server
app.listen(port => {
console.log(`Listening on port ${port}`);
});
};
Here's the current api-doc v2 I've created:
const apiDoc = {
swagger: "2.0",
info: {
title: "Node Express API.",
version: "0.1.0",
},
basePath: "/v2",
paths: {},
tags: [
{
name: "Users",
},
],
definitions: {
GeneralSuccess: {
type: "object",
properties: {
success: {
type: "boolean",
},
data: {
type: "object",
},
},
},
GeneralError: {
type: "object",
properties: {
success: {
type: "boolean",
},
error: {
type: "object",
properties: {
message: {
type: "string",
},
error: {
type: "object",
},
},
},
},
},
User: {
required: ["id"],
type: "object",
properties: {
id: {
type: "string",
description: "User's unique identifier",
},
email: {
type: "string",
description: "User's email address",
},
password: {
type: "string",
description: "User's password",
},
firstName: {
type: "string",
description: "User's firstname",
},
lastName: {
type: "string",
description: "User's lastname",
},
},
},
},
responses: {
200: {
// Success
description: "Successful request.",
schema: {
$ref: "#/definitions/GeneralSuccess",
},
},
404: {
// Not Found
description: "Entity not found.",
schema: {
$ref: "#/definitions/GeneralError",
},
},
422: {
// Illegal Input
description: "Illegal input for operation.",
schema: {
$ref: "#/definitions/GeneralError",
},
},
500: {
// Internal Server Error
description: "An unexpected error occurred",
schema: {
$ref: "#/definitions/GeneralError",
},
},
default: {
description: "Unexpected error",
schema: {
$ref: "#/definitions/GeneralError",
},
},
},
};
export default apiDoc;
And the routes are being specified under folder /routes/v2. I have two routes so far, users.js directly under the trunk that works (get, post) and users/{id}.js that triggers the same error every time. Content at this point is irrelevant as I copied/pasted the same code into a different file with a different name directly under v2 and it works.
So I'm getting at this point is more of an issue of configuration, some stupid detail I'm missing, or a package version conflict.
I appreciate any advice I can get to have this project running.
PS: one secondary issue, I think is a consequence, is that I'm not able to load any of the endpoints into the local SwaggerUI api-documentation page.
This is a bug in the open-api code: https://github.com/kogosoftwarellc/open-api/issues/896
The problem is that the dependency glob after a certain version works differently, and that surfaces this bug.
Easiest solution is probably to set version 7 of glob: npm equivalent of yarn resolutions?