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.”



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();
});
▶ Try it Yourself

▶ 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);
  });
});
▶ Try it Yourself

▶ 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);
});
▶ Try it Yourself

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;
▶ Try it Yourself

▶ 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 })
  });
};
▶ Try it Yourself
  1. 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 };
▶ Try it Yourself

▶ 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) => { /* ... */ });
▶ Try it Yourself

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() });
});
▶ Try it Yourself
  1. CI/CD process
100%
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.

📖 Summary

📝 Exercises

  1. Write at least 5 test cases covering authentication and CRUD operations for tasks, then run npm test to ensure they all pass.
  2. Add Swagger annotations to all routes. After starting the application, visit /api-docs to view the documentation.
  3. Create the Dockerfile and docker-compose.yml files, then run docker-compose up --build to verify the deployment.
  4. Add the /api/health health check endpoint and configure the HEALTHCHECK directive in Docker.
  5. Replace all instances of throw new Error() with the custom AppError class to ensure a consistent error response format.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏