Node.js: Task API Project (Part 3)
Last updated: 2026-08-26
1. Finalizing and Launching: Alice's Third Day
On the third day, Alice wrote tests and API documentation, while Bob set up the Docker deployment. “Just because the code runs doesn’t mean it’s ready for deployment,” Bob said. “Tests ensure quality, documentation ensures maintainability, and Docker ensures consistency.”
- Jest + Supertest is the standard combination for testing Node.js APIs
- Unified error handling ensures consistent API response formats
- Swagger automatically generates documentation; the code is the documentation.
- Docker containerization ensures consistency between development and production environments
- Health checks are a required configuration for production deployments
2. Testing with Jest and Supertest
(1) Testing the File Structure
| File | Description |
|---|---|
tests/setup.js |
Test Environment Configuration (Connecting to the Test Database) |
tests/auth.test.js |
Authentication Module Test |
tests/tasks.test.js |
Task Module Testing |
tests/helpers.js |
Test helper functions (creating test users, etc.) |
▶ Example: Test Environment Configuration
JAVASCRIPT
// tests/setup.js
process.env.JWT_SECRET = 'test-secret';
process.env.MONGO_URI = 'mongodb://localhost:27017/task-manager-test';
const mongoose = require('mongoose');
beforeAll(async () => await mongoose.connect(process.env.MONGO_URI));
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
▶ Example: Testing the Authentication Module
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
describe('Auth API', () => {
beforeEach(async () => await User.deleteMany({}));
test('should register a new user', async () => {
const res = await request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(201);
expect(res.body.token).toBeDefined();
expect(res.body.user.username).toBe('alice');
});
test('should login existing user', async () => {
await request(app).post('/api/auth/register').send({
username: 'alice', email: 'alice@test.com', password: '123456'
});
const res = await request(app).post('/api/auth/login').send({
email: 'alice@test.com', password: '123456'
});
expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
});
test('should reject invalid credentials', async () => {
const res = await request(app).post('/api/auth/login').send({
email: 'noone@test.com', password: 'wrong'
});
expect(res.status).toBe(401);
});
});
▶ Example: Testing the Task Module
JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register').send({
username: 'bob', email: 'bob@test.com', password: '123456'
});
token = reg.body.token;
userId = reg.body.user.id;
});
test('should create a task', async () => {
const res = await request(app).post('/api/tasks').set('Authorization', `Bearer ${token}`).send({ title: 'Write tests' });
expect(res.status).toBe(201);
expect(res.body.title).toBe('Write tests');
});
test('should get tasks list', async () => {
await Task.create({ title: 'Task1', assignedTo: userId });
const res = await request(app).get('/api/tasks').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(1);
});
test('should deny access without token', async () => {
const res = await request(app).get('/api/tasks');
expect(res.status).toBe(401);
});
3. Unified Encapsulation of Error Handling
▶ Example: Custom Error Class
JAVASCRIPT
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
module.exports = AppError;
▶ Example: Global Error Handling Middleware
JAVASCRIPT
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal Server Error';
res.status(statusCode).json({
status: statusCode >= 400 && statusCode < 500 ? 'fail' : 'error',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
- Swagger API Documentation
(1) Common Swagger Annotation Tags
| Tag | Description | Usage |
|---|---|---|
@openapi |
Path and Operation Definitions | Routing Files |
@swagger |
Component Definition (Schema) | Documentation Configuration |
@tags |
API Groups | Routing Files |
@security |
Authentication Method Reference | Endpoints Requiring Authentication |
@produces |
Response Format | Routing File |
@parameters |
Request Parameters | Route File |
▶ Example: Swagger Configuration
JAVASCRIPT
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0', description: 'Team Task Management RESTful API' },
servers: [{ url: 'http://localhost:3000/api' }],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
}
}
},
apis: ['./src/routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = { swaggerUi, specs };
▶ Example: Swagger Annotations in Routes
JAVASCRIPT
/**
* @openapi
* /auth/register:
* post:
* tags: [Auth]
* summary: User Registration
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [username, email, password]
* properties:
* username: { type: string }
* email: { type: string, format: email }
* password: { type: string, minLength: 6 }
* responses:
* 201:
* description: Registration Successful
* 400:
* description: Parameter error
*/
router.post('/register', async (req, res, next) => { /* ... */ });
4. Docker Deployment
(1) Docker File Description
| File | Description |
|---|---|
Dockerfile |
Building a Node.js Application Image |
docker-compose.yml |
Orchestration Application + MongoDB Container |
.dockerignore |
Exclude unnecessary files such as node_modules |
(2) Project Completion Checklist
| Check Item | Status |
|---|---|
| The project can be launched normally | ☐ |
| Sign Up/Log In API Available | ☐ |
| Task CRUD Available | ☐ |
| Pagination and filtering available | ☐ |
| Access control is functioning normally | ☐ |
| Test Passed | ☐ |
| Swagger documentation is accessible | ☐ |
| Docker build successful | ☐ |
| Health Check Endpoint Normal | ☐ |
| Unified Error Handling | ☐ |
Example: Dockerfile
DOCKERFILE
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
▶ Example: docker-compose.yml
YAML
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
ports:
- "27017:27017"
volumes:
mongo-data:
▶ Example: Health Check Endpoint
JAVASCRIPT
router.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() });
});
- CI/CD process
graph LR
A[Code Push] --> B[Run Jest Test]
B -->|Through| C[Build Docker Image]
B -->|Failure| D[Notice to Developers]
C --> E[Push the image to Registry]
E --> F[Deploy to the server]
F --> G[Health Checkup]
G -->|Through| H[Deployment Complete]
G -->|Failure| I[Rollback to a Previous Version]
▶ Example: Summary of Project Startup Commands
BASH
# Development Environment
npm run dev
# Run Test
npm test
# Docker Build
docker-compose up --build
# Production Deployment
docker-compose -f docker-compose.prod.yml up -d
5. Comprehensive Example: Testing + Documentation + Complete Docker Configuration
JAVASCRIPT
// tests/tasks.test.js
const request = require('supertest');
const app = require('../src/app');
const Task = require('../src/models/Task');
const User = require('../src/models/User');
let token, userId;
beforeEach(async () => {
await User.deleteMany({});
await Task.deleteMany({});
const reg = await request(app).post('/api/auth/register')
.send({ username: 'testuser', email: 'test@test.com', password: '123456' });
token = reg.body.token;
userId = reg.body.user.id;
});
describe('Task API', () => {
test('POST /api/tasks - create task', async () => {
const res = await request(app).post('/api/tasks')
.set('Authorization', `Bearer ${token}`)
.send({ title: 'My Task', priority: 'high' });
expect(res.status).toBe(201);
});
test('GET /api/tasks - list with pagination', async () => {
for (let i = 0; i < 15; i++) {
await Task.create({ title: `Task ${i}`, assignedTo: userId });
}
const res = await request(app).get('/api/tasks?page=2&limit=5')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.tasks.length).toBe(5);
expect(res.body.page).toBe(2);
});
test('DELETE /api/tasks/:id - owner can delete', async () => {
const task = await Task.create({ title: 'To Delete', assignedTo: userId });
const res = await request(app).delete(`/api/tasks/${task._id}`)
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
});
});
JAVASCRIPT
// src/config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const specs = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: { title: 'Task Manager API', version: '1.0.0' },
servers: [{ url: '/api' }],
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } } }
},
apis: ['./src/routes/*.js']
});
module.exports = { swaggerUi, specs };
DOCKERFILE
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]
YAML
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports: ["3000:3000"]
environment:
- MONGO_URI=mongodb://mongo:27017/task-manager
- JWT_SECRET=change_me_in_prod
depends_on: [mongo]
mongo:
image: mongo:7
volumes: [mongo-data:/data/db]
volumes:
mongo-data:
❓ FAQ
Q How do I choose between Jest and Mocha?
A Jest is ready to use out of the box, requires no configuration, and includes built-in assertions and code coverage; Mocha is more flexible but requires chai or sinon. For Node.js API testing, we recommend Jest + Supertest.
Q How do I test endpoints that require authentication in Supertest?
A First, call the login endpoint to obtain a token, then pass it in subsequent requests using .set('Authorization', 'Bearer ' + token).
Q Do I have to write Swagger documentation manually?
A You can use
swagger-jsdoc to automatically generate it from JSDoc comments, or use swagger-ui-express to display it. The comments serve as the documentation, so maintenance is minimal.Q How can I optimize the size of a Docker image?
A Use the Alpine base image, multi-stage builds (install dependencies during the build phase and copy only the artifacts during runtime), and a
.dockerignore file to exclude unnecessary files.Q How do I set up a CI/CD pipeline?
A For GitHub projects, use GitHub Actions: triggered by a push → install dependencies → run tests → build a Docker image → push to the repository → deploy to the server.
- Q: Why should API documentation be generated automatically? A: Manually maintaining documentation can easily become out of sync with the code. Swagger annotations are written directly in the code, so changing the code automatically updates the documentation, ensuring consistency.
- Q: How much should testing cover? A: For core business logic, we recommend covering at least 80%, including key paths such as registration, login, CRUD, and access control, as well as edge cases.
- Q: What are the advantages of Docker deployment over bare-metal deployment? A: Environment consistency (eliminating the "it works on my machine" issue), rapid deployment, resource isolation, ease of CI/CD integration, and horizontal scaling.
- Q: What should I be aware of in a production environment? A: Use a strong secret for JWT_SECRET, enable authentication in MongoDB, enable HTTPS, restrict CORS origins, set up rate limiting, and configure log collection.
- Q: How do I perform a health check? A: Provide the
/api/healthendpoint to return the application and database status; Docker HEALTHCHECK or Kubernetes livenessProbe calls this endpoint periodically. - Q: Will the test database conflict with the production database? A: Testing uses a separate database (such as
task-manager-test), and data is cleaned up before and after each test suite, so it will not affect production.
📖 Summary
- Wrap-up and Launch: Alice's Core Concepts and Usage on Day 3
- Core Concepts and Usage of Jest + Supertest for Testing
- Core Concepts and Usage of Unified Error Handling Encapsulation
- Core Concepts and Usage of Swagger API Documentation
- Core Concepts and Usage of Docker Deployment
- Core Concepts and Best Practices for CI/CD Workflows
- Comprehensive Example: Core Concepts and Usage of Testing + Documentation + Docker Complete Configuration
📝 Exercises
- Write at least 5 test cases covering authentication and CRUD operations for tasks, then run
npm testto ensure they all pass. - Add Swagger annotations to all routes. After starting the application, visit
/api-docsto view the documentation. - Create the Dockerfile and docker-compose.yml files, then run
docker-compose up --buildto verify the deployment. - Add the
/api/healthhealth check endpoint and configure the HEALTHCHECK directive in Docker. - Replace all instances of
throw new Error()with the customAppErrorclass to ensure a consistent error response format.