I’ve always believed that maintaining up-to-date API documentation is crucial for building reliable software. In my latest project, I wanted to integrate Swagger UI into my Express server, but with a twist—I needed the documentation to update automatically whenever I made changes to my API routes. After some research and experimentation, I discovered a workflow using swagger-autogen, chokidar-cli, and concurrently that worked perfectly for my TypeScript-based Express app. In this post, I’m going to share how I configured everything, loaded settings from my package.json, and set up automatic updates without relying on nodemon.
Thank me by sharing on Twitter 🙏
When I started working on this, I had a clear vision: I wanted the API docs to reflect every change in my routes immediately. I also wanted to avoid manual steps whenever I updated my endpoints. The first challenge was to generate the swagger.json file automatically using swagger-autogen. Next, I had to adjust the configuration so that the output was placed in the correct folder during the build process. Finally, I needed a mechanism to watch for file changes and trigger the documentation generation script. I’ll walk you through the steps I took and the reasoning behind each decision.
Setting Up Swagger-autogen with TypeScript Routes
My project already had a well-organized folder structure. The route files were located in the src/routes folder, and the Express server was built using TypeScript. I decided to use swagger-autogen because it automatically extracts documentation from my API routes, which are heavily annotated with inline comments.
I created a script file called swagger.js in a scripts folder. Here’s the key part of the script:
const swaggerAutogen = require('swagger-autogen')();
const path = require('path');
// Load package.json for dynamic settings
const pkg = require(path.join(__dirname, '../package.json'));
const doc = {
info: {
title: pkg.name, // Using the package name as the API title
description: pkg.description, // Using the package description for details
version: pkg.version // Keeping the version in sync with the package
},
host: 'localhost:3000',
schemes: ['http']
};
const outputFile = path.join(__dirname, '../dist/swagger.json');
const endpointsFiles = ['../src/routes/*.ts']; // Pointing to my TypeScript route files
swaggerAutogen(outputFile, endpointsFiles, doc).then(() => {
console.log('Swagger documentation has been generated in ./dist/swagger.json.');
});
In this script, I dynamically load package.json so that any changes to the title, description, or version in that file automatically reflect in my API documentation. This helps keep the documentation in sync with my project metadata without having to maintain multiple sources of truth. Moreover, by specifying endpointsFiles as ‘../src/routes/*.ts’, swagger-autogen scans all TypeScript route files, ensuring that every endpoint is documented.
HP 67 Black / Tri-color Ink Cartridges (2 Pack) | Works with HP DeskJet 1255, 2700, 4100 Series, HP ENVY 6000, 6400 Series | Eligible for Instant Ink | 3YP29AN
$36.89 (as of March 8, 2025 13:33 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Trapped in a Video Game (Volume 1)
$6.78 (as of March 7, 2025 13:30 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)HP 910 Cyan, Magenta, Yellow Ink Cartridges | Works with HP OfficeJet 8010, 8020 Series, HP OfficeJet Pro 8020, 8030 Series | Eligible for Instant Ink | 3YN97AN, 3 Count (Pack of 1)
$39.89 (as of March 8, 2025 13:33 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)I also made sure to output the swagger.json file to the dist folder, which is my production build directory. However, I knew that I needed to resolve potential path issues when deploying to production. Therefore, in my Express server, I resolved the path to the swagger.json file dynamically.
Dynamically Loading the Swagger JSON in Production
When deploying to production, relative paths can sometimes cause issues, so I opted for a dynamic approach. Instead of statically importing the swagger.json file using an import statement, I used Node’s path and fs modules to read the file at runtime. Here’s how I configured my Express app in TypeScript:
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import path from 'path';
import fs from 'fs';
const app = express();
// Dynamically resolve the path to swagger.json, which is one level up from __dirname
const swaggerPath = path.join(__dirname, "../swagger.json");
const swaggerDocument = JSON.parse(fs.readFileSync(swaggerPath, 'utf8'));
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.get('/hello', (req, res) => {
res.send('Hello World!');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
This approach ensures that, regardless of the build structure or hosting environment, my app always locates the swagger.json file correctly. By reading the file at runtime with fs.readFileSync, I avoid issues related to static imports that might break when the folder structure changes in production.
Automating Documentation Updates on File Changes
One of the biggest benefits of my setup is the automation of documentation updates. I didn’t want to rely on nodemon for this process, so I decided to use chokidar-cli to watch my route files for changes. I also used concurrently to run both the development server and the file watcher in parallel.
I installed the necessary packages as dev dependencies:
npm install --save-dev chokidar-cli concurrently
Then, I updated my package.json scripts to include the following entries:
"scripts": {
"build": "tsc && npm run build:swagger",
"build:swagger": "node scripts/swagger.js",
"swagger:watch": "chokidar 'src/routes/**/*.ts' -c 'npm run build:swagger'",
"dev": "concurrently \"dotenv -e .env -- tsx watch src/index.ts\" \"npm run swagger:watch\""
}
In this configuration, the “build:swagger” script generates the swagger.json file once by running the swagger.js script. The “swagger:watch” script uses chokidar-cli to monitor all TypeScript files in the src/routes folder. Whenever a change is detected, it triggers the “build:swagger” command. Finally, the “dev” script uses concurrently to run the development server alongside the file watcher. This means that whenever I make changes to my API routes, the swagger.json file updates automatically, keeping the API documentation current without any manual intervention.
Loading Settings from package.json for Consistency
One of the improvements I made to the documentation process was dynamically loading the API title, description, and version from the package.json file. This way, I only need to update the package metadata, and my API docs reflect those changes automatically. I accomplished this by requiring package.json in my swagger.js script, as shown earlier. This method not only reduces redundancy but also ensures consistency across the project.
By leveraging package.json data, I avoid hardcoding values in multiple places. This is particularly useful when versioning the API or when the project name changes during development. Since the swagger-autogen script pulls information directly from package.json, any updates to the package metadata immediately propagate to the generated documentation.
Handling File Paths in a Production Environment
In a production environment, file paths can be tricky, especially when the built files reside in a different directory structure than the source code. To address this, I used Node’s path module to construct an absolute path to the swagger.json file dynamically. Instead of relying on a relative import, I resolved the file path at runtime using __dirname. This ensures that no matter where the application is deployed, the path to swagger.json is calculated correctly.
I made a small yet critical adjustment in my Express server by using:
const swaggerPath = path.join(__dirname, "../swagger.json");
This line ensures that the server correctly locates the swagger.json file, even if the file structure changes after the build process. Additionally, by reading and parsing the file dynamically, I sidestep any issues that might arise from static imports when dealing with non-JavaScript assets.
Integrating Everything into a Seamless Build Process
After setting up swagger-autogen, configuring the Express server, and automating documentation updates with chokidar-cli, I integrated everything into my build process. I modified the build script in package.json so that the swagger documentation generation runs automatically during the build. The script I used was:
"build": "tsc && npm run build:swagger"
This ensures that whenever I build my project, the latest version of swagger.json is generated and ready for deployment. By having a single build command that compiles the TypeScript code and updates the API documentation, I reduce the risk of outdated documentation making it to production.
In my development setup, using concurrently to run both the TypeScript server and the file watcher simultaneously made it incredibly efficient to see real-time updates. Whenever I edited a route file, chokidar detected the change, and the swagger-autogen script re-generated the swagger.json file. This process helped me maintain high-quality documentation without any manual overhead.
Conclusion
Throughout this process, I learned a lot about integrating automated documentation into a modern TypeScript Express app. By using swagger-autogen, I was able to generate a swagger.json file that accurately reflects my API endpoints. I took the extra step to load API metadata directly from package.json, ensuring consistency across my project. Moreover, using chokidar-cli and concurrently allowed me to update the documentation automatically on file changes, which streamlined my development workflow without the need for nodemon.
Ultimately, this setup has significantly improved how I manage and maintain API documentation. It not only saves time but also ensures that the API documentation is always current, which is critical for developers and stakeholders alike. I hope that by sharing my experience, you find inspiration to implement similar practices in your projects, thereby enhancing your development process and overall project quality.