Modern APIs are only as good as their documentation. At Bonnetid, we maintain a fairly large and evolving Admin API that powers mosque management, prayer times, jamaat schedules, media uploads, and user administration. Keeping documentation accurate, discoverable, and always in sync with the codebase was critical for us.
This post walks through how we implemented Swagger (OpenAPI) documentation in our Node.js + Express backend, why we structured it the way we did, and how it scales as our API grows.
Why Swagger?
We chose Swagger (OpenAPI 3.0) because it gives us:
- Living documentation generated directly from code
- Built-in support for JWT authentication
- A UI to test APIs instantly
- A structured schema system reusable across endpoints
- Zero duplication between implementation and documentation
Most importantly, Swagger allows our frontend, mobile, and partner teams to work independently without constantly asking for API details.
High-Level Architecture
Our Swagger setup is intentionally decoupled from business logic:
server.js
swagger.js
routes/
├── jamaat.js
├── mosque.js
├── adhan-audio.js
└── …
controllers/
middlewares/
- Swagger configuration lives in one place (swagger.js)
- Route-level documentation lives next to the routes themselves
- Schemas are reusable across all endpoints
- Security rules match our real middleware behavior
This keeps things clean, predictable, and scalable.
Integrating Swagger into the Express Server
We expose Swagger as a public, read-only endpoint at /api-docs.
Server-level integration
In our main server file, Swagger is mounted just like any other Express middleware:
app.use(
‘/api-docs’,
swaggerUi.serve,
swaggerUi.setup(swaggerSpecs, {
customCss: ‘.swagger-ui .topbar { display: none }’,
customSiteTitle: ‘Bonnetid API Docs’,
swaggerOptions: {
persistAuthorization: true,
},
})
);
Why this matters?
- The documentation is always available
- Authorization tokens persist between requests
- We hide unnecessary UI elements for a cleaner look
- Works seamlessly in both development and production
Centralized Swagger Configuration
All OpenAPI configuration lives in a single file: swagger.js.
OpenAPI definition
definition: {
openapi: ‘3.0.0’,
info: {
title: ‘Bonnetid API’,
version: ‘1.0.0’,
description: ‘Complete CRUD API for Bonnetid app’
}
}
This ensures every endpoint automatically inherits:
- API metadata
- Versioning
- Description
- Environment-specific server URLs
Environment-Aware Server URLs
Swagger automatically points to the correct API base URL depending on the environment:
servers: [
{
url: process.env.NODE_ENV === ‘production’
? `https://${process.env.HOST}`
: `http://localhost:${process.env.PORT}`,
description: process.env.NODE_ENV === ‘production’
? ‘Production’
: ‘Development’
}
]
This allows developers to test APIs locally or in staging without changing anything.
JWT Authentication with Bearer Tokens
Since our platform uses Supabase authentication, all protected endpoints require a JWT. We define this once:
securitySchemes: {
bearerAuth: {
type: ‘http’,
scheme: ‘bearer’,
bearerFormat: ‘JWT’
}
}
And then reference it per route:
security:
– bearerAuth: []
Swagger UI automatically shows the Authorize button, allowing developers to paste a token and test protected endpoints instantly.
Reusable Data Schemas
One of the biggest advantages of Swagger is schema reuse.
We define complex models once, such as:
- Mosque
- JamaatPeriod
- Prayertime
- AdhanAudio
- HijriDates
- User
Example:
JamaatPeriod:
type: object
properties:
start_date:
type: string
format: date
fajr:
type: string
format: time
isha_offset:
type: integer
These schemas are then referenced across multiple endpoints using $ref, keeping documentation consistent and DRY.
Route-Level Documentation (Where Swagger Really Shines)
Instead of maintaining a separate documentation file, we document APIs right above the routes.
Example from our Jamaat module:
/**
* @swagger
* /api/admin/jamaat:
* get:
* summary: Get Jamaat period for mosque and date
* tags: [Jamaat]
* security:
* – bearerAuth: []
*/
router.get(‘/’, getJamaat);
Benefits of this approach
- Documentation lives next to the code
- Changes to routes naturally update docs
- Impossible for docs to drift out of sync
- Easy for new developers to understand APIs
Handling Permissions & Roles Transparently
Our backend enforces permissions using middleware such as:
- requireAuth
- requirePermission(“Jamaat Time”)
- requirePermission(“Masjid Admin”)
Swagger reflects this accurately by:
- Marking secured endpoints
- Grouping APIs by feature (tags)
This ensures the documentation matches real access control, not an idealized version.
Feature-Based API Grouping
Swagger automatically groups endpoints using tags like:
- Jamaat
- Mosque
- Users
- Prayertime
- Adhan Audio
- Hijri Calendar
This mirrors our folder structure and makes navigation intuitive even as the API grows.
Results & Impact
By integrating Swagger this way, we achieved:
- Reduced onboarding time for developers
- Always up-to-date documentation
- Faster debugging and testing
- Better collaboration across teams
- Confidence while shipping new features
Swagger is now a first-class citizen in our backend architecture—not an afterthought.
Final Thoughts
Good documentation is not something you write after building an API—it’s something you build into the API itself.
By combining Express, Swagger JSDoc, and disciplined structure, we created documentation that evolves naturally with our codebase and scales with our product.
If you’re building a serious API, Swagger isn’t optional—it’s infrastructure.
