Node.js: Task API Project (Part 1)
Last updated: 2026-08-26
1. Project Launch: Alice's First Day
Alice and her team received a new request—to build a team task management API. On the first day, they decided to start by setting up the project framework and the user authentication module. “Authentication is the foundation of everything,” Alice said. “Without authentication, the rest of the task management system would be meaningless.”
- Project initialization and directory structure design are the first steps in engineering.
- Mongoose model design forms the foundation of the data layer
- The combination of JWT and bcrypt is the mainstream solution for authentication in Node.js
- The Registration/Login API consists of the two core endpoints of the authentication module
- The
authmiddleware protects routes that require authentication
2. Project Initialization and Directory Structure
▶ Example: (1) Initialize an Express project
BASH
mkdir task-manager-api && cd task-manager-api
npm init -y
npm install express mongoose bcryptjs jsonwebtoken dotenv cors helmet
npm install --save-dev nodemon
▶ Example: (2) Directory Structure Design
TEXT
📖 Display only
task-manager-api/
├── src/
│ ├── config/
│ │ └── db.js
│ ├── middleware/
│ │ └── auth.js
│ ├── models/
│ │ ├── User.js
│ │ └── Task.js
│ ├── routes/
│ │ ├── auth.js
│ │ └── tasks.js
│ ├── validators/
│ │ └── authValidator.js
│ └── app.js
├── .env
├── .gitignore
├── package.json
└── server.js
| Directory/File | Description |
|---|---|
src/config/ |
Database connection, environment variables, and other configurations |
src/middleware/ |
Middleware for authentication, error handling, etc. |
src/models/ |
Mongoose Schema and Model Definitions |
src/routes/ |
Routing Modules, Categorized by Function |
src/validators/ |
Request Parameter Validation Logic |
src/app.js |
Express Main Application File |
server.js |
Input file, start the server |
▶ Example: server.js entry file
JAVASCRIPT
require('dotenv').config();
const app = require('./src/app');
const connectDB = require('./src/config/db');
connectDB();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
▶ Example: Database Connection Configuration
JAVASCRIPT
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB connected');
} catch (err) {
console.error('MongoDB connection error:', err.message);
process.exit(1);
}
};
module.exports = connectDB;
3. Mongoose Model Design
(1) User Model
| Field | Type | Required | Description |
|---|---|---|---|
username |
String | Yes | Username, unique index |
email |
String | Yes | Email, unique index |
password |
String | Yes | bcrypt-hashed password |
role |
String | No | Role: user (default) / admin |
createdAt |
Date | Automatic | Creation Date |
(2) Task Model
| Field | Type | Required | Description |
|---|---|---|---|
title |
String | Yes | Task Title |
description |
String | No | Task Details |
status |
String | No | pending (default) / in-progress / completed |
priority |
String | No | low (default) / medium / high |
assignedTo |
ObjectId | Yes | Assigned user, associated with User |
dueDate |
Date | No | Deadline |
createdAt |
Date | Automatic | Creation Date |
updatedAt |
Date | Automatic | Last Updated |
▶ Example: User Model Definition
JAVASCRIPT
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, minlength: 6 },
role: { type: String, enum: ['user', 'admin'], default: 'user' }
}, { timestamps: true });
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
userSchema.methods.comparePassword = function (candidate) {
return bcrypt.compare(candidate, this.password);
};
module.exports = mongoose.model('User', userSchema);
▶ Example: Task Model Definition
JAVASCRIPT
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
description: { type: String, default: '' },
status: { type: String, enum: ['pending', 'in-progress', 'completed'], default: 'pending' },
priority: { type: String, enum: ['low', 'medium', 'high'], default: 'low' },
assignedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
dueDate: { type: Date }
}, { timestamps: true });
module.exports = mongoose.model('Task', taskSchema);
4. User Authentication Module
▶ Example: (1) How JWT Authentication Works
graph TD
A[User Registration/Log In] --> B[Server Authentication Credentials]
B --> C[Generate JWT Token]
C --> D[Back Token To the client]
D --> E[Client-side Token Request]
E --> F[auth Middleware Validation Token]
F -->|Valid| G[Forward to the routing processor]
F -->|Invalid| H[Back 401 Error]
(2) Design of Authentication API Endpoints
| Method | Path | Description | Authentication Required |
|---|---|---|---|
| POST | /api/auth/register |
User Registration | No |
| POST | /api/auth/login |
User Login | No |
| GET | /api/auth/me |
Get current user | Yes |
▶ Example: Registration and Login Routes
JAVASCRIPT
const router = require('express').Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const generateToken = (id) => jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '7d' });
router.post('/register', async (req, res, next) => {
try {
const { username, email, password } = req.body;
const user = await User.create({ username, email, password });
res.status(201).json({ token: generateToken(user._id), user: { id: user._id, username, email, role: user.role } });
} catch (err) {
next(err);
}
});
router.post('/login', async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ message: 'Invalid credentials' });
}
res.json({ token: generateToken(user._id), user: { id: user._id, username: user.username, email, role: user.role } });
} catch (err) {
next(err);
}
});
module.exports = router;
▶ Example: auth middleware
JAVASCRIPT
const jwt = require('jsonwebtoken');
const User = require('../models/User');
module.exports = async (req, res, next) => {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ message: 'No token provided' });
}
try {
const decoded = jwt.verify(header.split(' ')[1], process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
if (!req.user) return res.status(401).json({ message: 'User not found' });
next();
} catch {
res.status(401).json({ message: 'Invalid token' });
}
};
▶ Example: Get the current user's route
JAVASCRIPT
const auth = require('../middleware/auth');
router.get('/me', auth, async (req, res) => {
res.json({ user: { id: req.user._id, username: req.user.username, email: req.user.email, role: req.user.role } });
});
5. The app.js Main File and .env Configuration
▶ Example: Integrating app.js
JAVASCRIPT
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const authRoutes = require('./routes/auth');
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({ message: err.message || 'Server Error' });
});
module.exports = app;
▶ Example: .env Environment Variables
TEXT
📖 Display only
PORT=3000
MONGO_URI=mongodb://localhost:27017/task-manager
JWT_SECRET=your_super_secret_key_change_in_production
6. Comprehensive Example: The Complete Project Initialization Process
The following code ties all the core modules mentioned above together. With about 80 lines of core code, you can run a basic task management API with authentication:
JAVASCRIPT
// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const helmet = require('helmet');
const app = express();
app.use(helmet(), cors(), express.json());
// --- Models ---
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' }
}, { timestamps: true });
userSchema.pre('save', async function () { if (this.isModified('password')) this.password = await bcrypt.hash(this.password, 10); });
userSchema.methods.comparePassword = function (pw) { return bcrypt.compare(pw, this.password); };
const User = mongoose.model('User', userSchema);
// --- Auth Middleware ---
const auth = async (req, res, next) => {
try {
const decoded = jwt.verify(req.headers.authorization?.split(' ')[1], process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch { res.status(401).json({ message: 'Unauthorized' }); }
};
// --- Routes ---
app.post('/api/auth/register', async (req, res) => {
const user = await User.create(req.body);
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
res.status(201).json({ token, user: { id: user._id, username: user.username, role: user.role } });
});
app.post('/api/auth/login', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user || !(await user.comparePassword(req.body.password))) return res.status(401).json({ message: 'Invalid credentials' });
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
res.json({ token, user: { id: user._id, username: user.username, role: user.role } });
});
app.get('/api/auth/me', auth, (req, res) => res.json(req.user));
// --- Start ---
mongoose.connect(process.env.MONGO_URI).then(() => {
app.listen(process.env.PORT || 3000, () => console.log('Server running'));
});
❓ FAQ
Q How should the project directory structure be designed?
A Organize it by functionality: store data models in
models/, routes in routes/, middleware in middleware/, and configurations in config/. Place the entry point, app.js, in the root directory.Q Why use Mongoose instead of the native driver?
A Mongoose provides schema validation, middleware hooks, type hints, and a query builder, which boosts development efficiency; the native driver is lighter and better suited for simple scenarios.
Q Should the .env file be committed to Git?
A Absolutely not. The .env file contains sensitive information such as database passwords and keys, so it should be added to .gitignore and injected via environment variables or CI during deployment.
Q How long should the JWT secret be?
A It should be a random string of at least 32 characters; for production environments, 64 characters or more is recommended. You can generate one using
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))".Q How do I test the registration API?
A Use Postman or curl to send a POST request to /api/auth/register, check the returned token, and then use that token to access a protected route to verify that authentication is working.
- Q: Why do we need to authenticate first? A: Authentication is a prerequisite for business logic; without an identity, it is impossible to establish data ownership and control access permissions, and all subsequent CRUD operations depend on authentication.
- Q: Is password storage secure? A: bcrypt uses adaptive hashing with a salt, which is far superior to plaintext or MD5/SHA. With 10 rounds, the cost is approximately 100 ms per operation, striking a balance between security and performance.
- Q: How should the JWT secret be managed? A: Use a .env file in the development environment and environment variables or a secret management service (such as AWS Secrets Manager) in the production environment; never hard-code it.
- Q: How do I test registration/login? A: Use Postman or curl to send a POST request to
/api/auth/registerand/api/auth/login, and check the returned token. - Q: How should the project structure be organized? A: Break it down into MVC components: models (data), routes, middleware, and config, to maintain separation of concerns.
- Q: Which is better, bcrypt or crypto? A: bcrypt is specifically designed for passwords and includes built-in salt values and an adaptive cost, making it more suitable for password hashing than the SHA series in the crypto package.
📖 Summary
- Project Launch: Key Concepts and How to Use Alice on Day One
- Core Concepts and Usage of Project Initialization and Directory Structures
- Core Concepts and Usage of Mongoose Model Design
- Core Concepts and Usage of the User Authentication Module
- Core Concepts and Usage of the app.js Main File and the .env Configuration File
- Comprehensive Example: Core Concepts and Usage of the Entire Project Initialization Process
📝 Exercises
- Initialize the project according to the directory structure, install all dependencies, and ensure that
npm run devcan start the server. - Create two Mongoose models,
UserandTask, and use Postman to test the registration and login APIs. - Write an auth middleware and test the
/api/auth/meendpoint: It should return a 401 error if no token is provided, and return the user's information if a valid token is provided. - Configure JWT_SECRET and MONGO_URI in
.env, and ignore the.envfile in.gitignore.