APIs power modern web applications, and building one that’s robust, secure, and maintainable is critical for production environments. In this tutorial, we'll build a production-ready Node.js API that uses OpenAPI for standardized documentation and request/response validation. Our project features two versions of the API—with v1 endpoints secured using JWT authentication and v2 endpoints open for testing. We’ve also integrated middleware for security, rate limiting, CORS, logging, and error handling.
You can check out the full project on GitHub and see the hosted API docs on Render.
Table of Contents
Project Overview
In our project, we have set up two versions of the API:
- v1 (Protected): These endpoints are secured with JWT authentication. You’ll need to register and log in to receive a token, which grants you access to protected routes.
- v2 (Open): These endpoints are open to all, making it easier to test and experiment with API calls.
Both versions share a common functionality: managing an in-memory array of resource objects. The expected structure of each resource is:
{
"id": 0,
"name": "string",
"value": "string"
}
Each API endpoint (GET, POST, PUT, PATCH, DELETE) adheres strictly to this schema using OpenAPI validation.
Getting Started
Prerequisites
- Node.js (v14 or higher)
- npm (Node Package Manager)
Installation
- Clone the Repository:
git clone https://github.com/rabindratamang/openapi-project.git
cd openapi-project
- Install Dependencies:
npm install
- Environment Variables:
Create a .env
file in the project root with the following content:
PORT=3000
JWT_SECRET=your_jwt_secret_key
- Start the Server:
npm start
The server will start on http://localhost:3000, and you can access the API documentation at http://localhost:3000/api-docs.
Understanding the Code
Project Structure
Our project is organized to ensure scalability and maintainability:
openapi-project/
│-- src/
│ │-- config/
│ │ └── auth.js # JWT configuration
│ │-- controllers/
│ │ ├── authController.js # Authentication logic
│ │ ├── v1Controller.js # Business logic for v1 resources
│ │ └── v2Controller.js # Business logic for v2 resources
│ │-- routes/
│ │ ├── v1Routes.js # Routes for API v1 (protected)
│ │ └── v2Routes.js # Routes for API v2 (open)
│ │-- middlewares/
│ │ └── errorHandler.js # Centralized error handling
│ │-- utils/
│ │ └── logger.js # Logging utility
│ └── app.js # Express app initialization (if needed)
│-- swagger/
│ └── swagger.yaml # OpenAPI spec defining endpoints, schemas, and security
│-- .env
│-- .gitignore
│-- package.json
│-- README.md
│-- server.js # Entry point for the server
This structure helps you to easily extend your application and keep the concerns separate (routing, business logic, error handling, etc.).
Swagger/OpenAPI Specification
Our swagger/swagger.yaml
file is the cornerstone of the API documentation and validation. It defines:
- Security Schemes: JWT authentication for v1 endpoints.
- Resource Schema: Enforcing a strict schema for every resource object.
Here’s a snippet from our OpenAPI spec:
components:
schemas:
Resource:
type: object
required:
- id
- name
- value
properties:
id:
type: integer
example: 0
name:
type: string
example: "string"
value:
type: string
example: "string"
This schema guarantees that any object processed by our API exactly follows this structure. If a request has extra or missing fields, the express-openapi-validator
middleware will reject it.
JWT Authentication & API Versioning
For v1 endpoints, we secure routes with JWT. The authentication flow typically involves:
- User Registration: Creating a new user and generating a JWT token.
- User Login: Validating credentials and returning a JWT token.
- Protected Routes: Middleware checks the presence and validity of the token before processing the request.
In src/routes/v1Routes.js
, routes are defined like so:
const express = require("express");
const router = express.Router();
const v1Controller = require("../controllers/v1Controller");
// Protected routes (JWT authentication middleware applied globally or per-route)
router.get("/resources", v1Controller.getAllResources);
router.post("/resources", v1Controller.createResource);
router.put("/resources/:id", v1Controller.updateResource);
router.patch("/resources/:id", v1Controller.modifyResource);
router.delete("/resources/:id", v1Controller.deleteResource);
module.exports = router;
The OpenAPI spec for these endpoints references the security scheme:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
For v2 endpoints (in src/routes/v2Routes.js
), no authentication is required, making it ideal for testing or public APIs.
OpenAPI Validation
To ensure that every request conforms to our defined schemas, we integrate express-openapi-validator
. This middleware reads the swagger.yaml
file and validates:
-
Request Bodies: Ensuring they contain the exact fields (
id
,name
,value
). - Responses: Confirming that responses match the spec.
In server.js
, we add:
const { OpenApiValidator } = require("express-openapi-validator");
const path = require("path");
app.use(
OpenApiValidator.middleware({
apiSpec: path.join(__dirname, "swagger", "swagger.yaml"),
validateRequests: true,
validateResponses: true,
})
);
With this setup, if a client sends a request that doesn't match the schema (for example, missing the name
property), the API responds with a clear error message.
Middleware for Security and Logging
We’ve added several middleware packages to enhance security and reliability:
- Helmet: Sets various HTTP headers to protect the app.
-
Rate Limiter: (Not detailed here, but you can add
express-rate-limit
to limit request frequency.) - CORS: Allows cross-origin requests.
- Morgan: Logs HTTP requests.
- Error Handler: Centralizes error responses to make debugging easier.
All these are configured in server.js
:
app.use(express.json());
app.use(cors());
app.use(helmet());
app.use(morgan("combined"));
Centralized error handling in src/middlewares/errorHandler.js
ensures that any unexpected issues are caught and sent back to the client in a consistent format.
Running and Testing the API
Start the Server:
Runnpm start
and open http://localhost:3000/api-docs to interact with the API via Swagger UI.Register and Login (v1):
Use the/api/auth/register
and/api/auth/login
endpoints to receive a JWT token, then use that token to access protected v1 endpoints.-
Try Out Endpoints:
- GET /api/v1/resources: Retrieve all resources (requires JWT).
- POST /api/v1/resources: Add a new resource (requires JWT).
- PUT/PATCH/DELETE /api/v1/resources/{id}: Modify or delete a resource by its ID (requires JWT).
- v2 endpoints function similarly but are open and do not require JWT.
Validation in Action:
Test the OpenAPI validator by sending an invalid payload. For example, omitting thename
field should result in an error response detailing the missing property.
Wrapping Up
Building a production-ready API means planning for both functionality and security from the very start. With our Node.js project, you’ve learned how to:
- Structure your application for scalability.
- Document your API using OpenAPI/Swagger.
- Secure endpoints with JWT authentication.
- Enforce strict request/response validation using
express-openapi-validator
. - Enhance security with Helmet, CORS, and logging middleware.
I hope this step-by-step guide helps you build robust APIs in your projects. Check out the GitHub repository for the full code, and feel free to share your thoughts and improvements.
Top comments (0)