Building the Backend API with Node.js and Express
Last updated September 18, 2023
JavaScript

Guide Parts
Building the Backend API with Node.js and Express
In this section, we'll build a RESTful API using Node.js, Express, and MongoDB. Our API will handle user authentication, data validation, and CRUD operations for our application's core features.
Database Schema Design
Let's start by designing our MongoDB schemas. For this project, we'll create a task management application with users and tasks.
Setting Up the Models Directory
First, create a models directory in your server folder:
mkdir -p server/models
User Model
Create a file named User.js
in the models directory:
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Please provide a name"],
trim: true,
maxlength: [50, "Name cannot be more than 50 characters"],
},
email: {
type: String,
required: [true, "Please provide an email"],
match: [
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
"Please provide a valid email",
],
unique: true,
lowercase: true,
trim: true,
},
password: {
type: String,
required: [true, "Please provide a password"],
minlength: [6, "Password must be at least 6 characters"],
select: false,
},
role: {
type: String,
enum: ["user", "admin"],
default: "user",
},
createdAt: {
type: Date,
default: Date.now,
},
});
// Hash password before saving
UserSchema.pre("save", async function (next) {
// Only hash the password if it's modified
if (!this.isModified("password")) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to compare passwords
UserSchema.methods.comparePassword = async function (candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model("User", UserSchema);
Task Model
Create a file named Task.js
in the models directory:
const mongoose = require("mongoose");
const TaskSchema = new mongoose.Schema({
title: {
type: String,
required: [true, "Please provide a task title"],
trim: true,
maxlength: [100, "Title cannot be more than 100 characters"],
},
description: {
type: String,
trim: true,
maxlength: [500, "Description cannot be more than 500 characters"],
},
status: {
type: String,
enum: ["pending", "in-progress", "completed"],
default: "pending",
},
priority: {
type: String,
enum: ["low", "medium", "high"],
default: "medium",
},
dueDate: {
type: Date,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
// Create index for faster queries
TaskSchema.index({ user: 1, status: 1 });
module.exports = mongoose.model("Task", TaskSchema);
Authentication Middleware
Now let's create middleware to handle authentication using JSON Web Tokens (JWT).
Create the Middleware Directory
mkdir -p server/middleware
Auth Middleware
Create a file named auth.js
in the middleware directory:
const jwt = require("jsonwebtoken");
const User = require("../models/User");
// Protect routes
exports.protect = async (req, res, next) => {
let token;
// Check if token exists in headers
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
token = req.headers.authorization.split(" ")[1];
}
// Make sure token exists
if (!token) {
return res.status(401).json({
success: false,
message: "Not authorized to access this route",
});
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user to request object
req.user = await User.findById(decoded.id).select("-password");
next();
} catch (error) {
return res.status(401).json({
success: false,
message: "Not authorized to access this route",
});
}
};
// Check user role
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: `User role ${req.user.role} is not authorized to access this route`,
});
}
next();
};
};
Controllers
Now, let's create the controllers that will handle our API requests.
Create Controllers Directory
mkdir -p server/controllers
Auth Controller
Create a file named authController.js
in the controllers directory:
const User = require("../models/User");
const jwt = require("jsonwebtoken");
const { validationResult } = require("express-validator");
// Generate JWT token
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: "30d",
});
};
// @desc Register a new user
// @route POST /api/auth/register
// @access Public
exports.register = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { name, email, password } = req.body;
try {
// Check if user already exists
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
success: false,
message: "User already exists",
});
}
// Create user
user = await User.create({
name,
email,
password,
});
// Generate token
const token = generateToken(user._id);
res.status(201).json({
success: true,
token,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
// @desc Login user
// @route POST /api/auth/login
// @access Public
exports.login = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
try {
// Check if user exists
const user = await User.findOne({ email }).select("+password");
if (!user) {
return res.status(401).json({
success: false,
message: "Invalid credentials",
});
}
// Check if password matches
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({
success: false,
message: "Invalid credentials",
});
}
// Generate token
const token = generateToken(user._id);
res.json({
success: true,
token,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
// @desc Get current logged in user
// @route GET /api/auth/me
// @access Private
exports.getMe = async (req, res) => {
try {
const user = await User.findById(req.user.id);
res.json({
success: true,
user: {
id: user._id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
Task Controller
Create a file named taskController.js
in the controllers directory:
const Task = require("../models/Task");
const { validationResult } = require("express-validator");
// @desc Get all tasks for a user
// @route GET /api/tasks
// @access Private
exports.getTasks = async (req, res) => {
try {
const tasks = await Task.find({ user: req.user.id }).sort({
createdAt: -1,
});
res.json({
success: true,
count: tasks.length,
data: tasks,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
// @desc Get a single task
// @route GET /api/tasks/:id
// @access Private
exports.getTask = async (req, res) => {
try {
const task = await Task.findById(req.params.id);
if (!task) {
return res.status(404).json({
success: false,
message: "Task not found",
});
}
// Check if user owns the task
if (task.user.toString() !== req.user.id && req.user.role !== "admin") {
return res.status(403).json({
success: false,
message: "Not authorized to access this task",
});
}
res.json({
success: true,
data: task,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
// @desc Create a new task
// @route POST /api/tasks
// @access Private
exports.createTask = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
// Add user to request body
req.body.user = req.user.id;
// Create task
const task = await Task.create(req.body);
res.status(201).json({
success: true,
data: task,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
// @desc Update a task
// @route PUT /api/tasks/:id
// @access Private
exports.updateTask = async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
let task = await Task.findById(req.params.id);
if (!task) {
return res.status(404).json({
success: false,
message: "Task not found",
});
}
// Check if user owns the task
if (task.user.toString() !== req.user.id && req.user.role !== "admin") {
return res.status(403).json({
success: false,
message: "Not authorized to update this task",
});
}
// Update task
task = await Task.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true,
});
res.json({
success: true,
data: task,
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
// @desc Delete a task
// @route DELETE /api/tasks/:id
// @access Private
exports.deleteTask = async (req, res) => {
try {
const task = await Task.findById(req.params.id);
if (!task) {
return res.status(404).json({
success: false,
message: "Task not found",
});
}
// Check if user owns the task
if (task.user.toString() !== req.user.id && req.user.role !== "admin") {
return res.status(403).json({
success: false,
message: "Not authorized to delete this task",
});
}
await task.remove();
res.json({
success: true,
data: {},
});
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: "Server Error",
});
}
};
Routes
Now, let's set up the routes for our API.
Create Routes Directory
mkdir -p server/routes
Auth Routes
Create a file named auth.js
in the routes directory:
const express = require("express");
const router = express.Router();
const { check } = require("express-validator");
const { register, login, getMe } = require("../controllers/authController");
const { protect } = require("../middleware/auth");
// Register route with validation
router.post(
"/register",
[
check("name", "Name is required").not().isEmpty(),
check("email", "Please include a valid email").isEmail(),
check(
"password",
"Please enter a password with 6 or more characters"
).isLength({ min: 6 }),
],
register
);
// Login route with validation
router.post(
"/login",
[
check("email", "Please include a valid email").isEmail(),
check("password", "Password is required").exists(),
],
login
);
// Get current user route (protected)
router.get("/me", protect, getMe);
module.exports = router;
Task Routes
Create a file named tasks.js
in the routes directory:
const express = require("express");
const router = express.Router();
const { check } = require("express-validator");
const {
getTasks,
getTask,
createTask,
updateTask,
deleteTask,
} = require("../controllers/taskController");
const { protect } = require("../middleware/auth");
// Protect all routes
router.use(protect);
// Get all tasks
router.get("/", getTasks);
// Get single task
router.get("/:id", getTask);
// Create task with validation
router.post(
"/",
[
check("title", "Title is required").not().isEmpty(),
check("description", "Description cannot be more than 500 characters")
.optional()
.isLength({ max: 500 }),
check("status", "Status is invalid")
.optional()
.isIn(["pending", "in-progress", "completed"]),
check("priority", "Priority is invalid")
.optional()
.isIn(["low", "medium", "high"]),
],
createTask
);
// Update task with validation
router.put(
"/:id",
[
check("title", "Title is required").optional().not().isEmpty(),
check("description", "Description cannot be more than 500 characters")
.optional()
.isLength({ max: 500 }),
check("status", "Status is invalid")
.optional()
.isIn(["pending", "in-progress", "completed"]),
check("priority", "Priority is invalid")
.optional()
.isIn(["low", "medium", "high"]),
],
updateTask
);
// Delete task
router.delete("/:id", deleteTask);
module.exports = router;
Updating the Main Server File
Now let's update our main index.js
file to include our routes:
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const mongoose = require("mongoose");
const helmet = require("helmet");
const morgan = require("morgan");
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middlewares
app.use(cors());
app.use(express.json());
app.use(helmet());
app.use(morgan("dev"));
// Import routes
const authRoutes = require("./routes/auth");
const taskRoutes = require("./routes/tasks");
// Mount routes
app.use("/api/auth", authRoutes);
app.use("/api/tasks", taskRoutes);
// Basic route for testing
app.get("/api", (req, res) => {
res.json({ message: "API is working!" });
});
// Error handling middleware
app.use((req, res, next) => {
const error = new Error(`Not found - ${req.originalUrl}`);
res.status(404);
next(error);
});
app.use((error, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
success: false,
message: error.message,
stack: process.env.NODE_ENV === "production" ? null : error.stack,
});
});
// Connect to MongoDB
mongoose
.connect(process.env.MONGO_URI)
.then(() => console.log("Connected to MongoDB"))
.catch((err) => console.error("Could not connect to MongoDB:", err));
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
Testing the API
Let's test our API endpoints using a tool like Postman or Thunder Client in VS Code:
- Register a new user:
- POST http://localhost:5000/api/auth/register
- Body:
{ "name": "John Doe", "email": "john@example.com", "password": "123456" }
- Login with the user:
- POST http://localhost:5000/api/auth/login
- Body:
{ "email": "john@example.com", "password": "123456" }
- Create a new task:
- POST http://localhost:5000/api/tasks
- Headers:
Authorization: Bearer <token_from_login>
- Body:
{ "title": "Complete API", "description": "Finish building the RESTful API", "priority": "high" }
- Get all tasks:
- GET http://localhost:5000/api/tasks
- Headers:
Authorization: Bearer <token_from_login>
In the next part, we'll build the React frontend that will interact with our API.
Read previous part
Building the React Frontend
Read next part
Introduction to Docker: Containerization Made Simple
Enjoyed the read? Help us spread the word — say something nice!
Guide Parts
Enjoyed the read? Help us spread the word — say something nice!
Guide Parts