typescriptexpressswaggerswagger-uivercel

Swagger : No operations defined in spec!, Express Typescript on Vercel


I'm using Swagger, and everything was working perfectly on my localhost. However, after deploying my project to Vercel, Swagger no longer works at all. I encountered the following error:

Blank Page

After reading a post on Stack Overflow (unfortunately, I can't find the link anymore), I tried switching my "swagger-ui-express" version to 4.6.2. This partially solved the issue: now, I can access the Swagger UI, but no operations are found in specs.

No operations defined in spec!

Here are more details about my configuration:

Tree

My project tree

src/index.ts :

import express, { Express, Request, Response } from 'express';
import 'dotenv/config';
import { setupCors } from './utils/cors';
import { setupSwagger } from './utils/swagger';
import { logger, setupLogger } from './utils/logger';
import { setupRoutes } from './utils/routes';

const app: Express = express();

// JSON Parser
app.use(express.json());

// CORS
setupCors(app);

// Logger
setupLogger(app);

// Swagger
setupSwagger(app);

// Routes
setupRoutes(app);

// Start server
const port = process.env.PORT || 3000;
app.listen(port, () => {
  logger.info(`Server running : http://localhost:${port}`);
  logger.info(`Swagger running : http://localhost:${port}/api-docs`);
  logger.info('Server started !');
});

src/utils/swagger.ts

import swaggerJSDoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { Application } from 'express';

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'App',
      version: '0.0.0',
      description: 'An app',
    },
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        },
      },
    },
  },
  apis: ['./src/routes/*.ts'],
};

const CSS_URL =
  'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.0/swagger-ui.min.css';
const swaggerSpec = swaggerJSDoc(options);

export const setupSwagger = (app: App`your text`lication) => {
  app.use(
    '/api-docs',`your text`
    swaggerUi.serve,
    swaggerUi.setup(swaggerSpec, {
      customCss:
        '.swagger-ui .opblock .opblock-summary-path-description-wrapper { align-items: center; display: flex; flex-wrap: wrap; gap: 0 10px; padding: 0 10px; width: 100%; }',
      customCssUrl: CSS_URL,
    }),
  );
};

src/routes/auth.ts

import express from 'express';
import { signUp, signIn } from '../controllers/auth';
import {
  validateEmail,
  validateName,
  validatePassword,
} from '../validator/signupSchema';
import { fieldsValidation } from '../middlewares/fieldsValidation';
import { authorizeAccess } from '../middlewares/routesAuthorization';

export const authRouter = express.Router();

/**
 * @swagger
 * /auth/sign-up:
 *   post:
 *     summary: Sign up a new user
 *     tags: [Auth]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name:
 *                 type: string
 *               email:
 *                 type: string
 *               password:
 *                 type: string
 *     responses:
 *       200:
 *         description: User signed up successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 message:
 *                   type: string
 *                   example: User signed up successfully
 *                 data:
 *                   type: object
 *                   properties:
 *                     id:
 *                       type: integer
 *                       example: 5
 *                     name:
 *                       type: string
 *                       example: John Doe
 *                     email:
 *                       type: string
 *                       example: johndoe@example.com
 *                     password_hash:
 *                       type: string
 *                       example: "$2b$10$tnHGADEJL0QDYDdkq3YeQeGVvirjwKfaWIGXtjYJiFCBniyxYpgRe"
 *                     role:
 *                       type: string
 *                       example: customer
 *                     created_at:
 *                       type: string
 *                       example: "2025-02-23T02:05:22.023Z"
 *       400:
 *         description: Invalid input (e.g., password doesn't meet security requirements)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: Password must contain at least one uppercase letter.
 *       409:
 *         description: User already exists (e.g., email already registered)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: User already exists
 *       500:
 *         description: Internal server error (e.g., unexpected failure on the server side)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: Internal server error
 */

authRouter.post(
  '/sign-up',
  [validateName, validateEmail, validatePassword],
  fieldsValidation,
  signUp,
);

/**
 * @swagger
 * /auth/sign-in:
 *   post:
 *     summary: Sign in a user
 *     tags: [Auth]
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               email:
 *                 type: string
 *               password:
 *                 type: string
 *     responses:
 *       200:
 *         description: User signed in successfully
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 message:
 *                   type: string
 *                   example: User johndoe@example.com is authenticated
 *                 data:
 *                   type: object
 *                   properties:
 *                     id:
 *                       type: integer
 *                       example: 4
 *                     name:
 *                       type: string
 *                       example: John Doe
 *                     email:
 *                       type: string
 *                       example: johndoe@example.com
 *                     password_hash:
 *                       type: string
 *                       example: "$2b$10$z/1y41pGVlsgxhAZL7GHEuutbo3c1NJFWDZ4TPCZjRzehQvLMVoku"
 *                     role:
 *                       type: string
 *                       example: customer
 *                     created_at:
 *                       type: string
 *                       example: "2025-02-23T00:44:53.309Z"
 *                     token:
 *                       type: string
 *                       example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huZG9lQGV4YW1wbGUuY29tIiwicm9sZSI6ImN1c3RvbWVyIiwiaWF0IjoxNzQwMjc2NjUwLCJleHAiOjE3NDAzNjMwNTAsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJ9.dte0uLjohUur7bjgtT21IzXutNx8R3miRHcLJc05-w4"
 *       400:
 *         description: Invalid input (e.g., email doesn't meet requirements)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: Bad email format.
 *       401:
 *         description: Invalid credentials (e.g., user doesn't exist or password is incorrect)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: Invalid credentials.
 *       500:
 *         description: Internal server error (e.g., unexpected failure on the server side)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 message:
 *                   type: string
 *                   example: Internal server error
 */
authRouter.post(
  '/sign-in',
  [validateEmail, validatePassword],
  fieldsValidation,
  signIn,
);

package.json

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon src/index.ts",
    "build": "tsc -p ."
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "@types/bcrypt": "^5.0.2",
    "bcrypt": "^5.1.1",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "express-validator": "^7.2.1",
    "jsonwebtoken": "^9.0.2",
    "morgan": "^1.10.0",
    "pg": "^8.13.3",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^4.6.2",
    "winston": "^3.17.0",
    "z-schema": "^6.0.2"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^5.0.0",
    "@types/jsonwebtoken": "^9.0.9",
    "@types/morgan": "^1.9.9",
    "@types/node": "^22.13.4",
    "@types/pg": "^8.11.11",
    "@types/swagger-jsdoc": "^6.0.4",
    "@types/swagger-ui-express": "^4.1.8",
    "nodemon": "^3.1.9",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.3"
  }
}

vercel.json

{
  "version": 2,
  "builds": [
    {
      "src": "src/index.ts",
      "use": "@vercel/node"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "src/index.ts"
    }
  ]
}

Solution

  • You just have to change :

      apis: ['./src/routes/*.ts'],
    

    to :

      apis: ['./src/routes/*.js'],
    

    in your "src/utils/swagger.ts" file