Build RESTful API (CRUD + Auth)
Now it's time to put everything together! In this chapter we'll build a complete RESTful API with authentication and full CRUD operations. This is a real-world project that combines everything you've learned so far.
Project Overview: Task Manager API
We'll build a Task Manager API where users can:
- Register and login (JWT authentication).
- Create read update and delete tasks.
- Only see their own tasks.
- Mark tasks as complete/incomplete.
This project combines authentication database integration CRUD operations and security everything a real API needs.
Project Setup
mkdir task-manager-apicd task-manager-apinpm init -ynpm install express mongoose bcrypt jsonwebtoken dotenv express-rate-limit helmetnpm install --save-dev nodemonFolder Structure
task-manager-api/├── models/│ ├── User.js│ └── Task.js├── middleware/│ ├── auth.js│ └── errorHandler.js├── controllers/│ ├── authController.js│ └── taskController.js├── routes/│ ├── authRoutes.js│ └── taskRoutes.js├── utils/│ ├── catchAsync.js│ └── AppError.js├── config/│ └── db.js├── .env├── app.js└── server.js1. Configuration Files
.env
NODE_ENV=developmentPORT=3000MONGODB_URI=mongodb://localhost:27017/task-managerJWT_SECRET=your-super-secret-jwt-key-change-thisJWT_EXPIRES_IN=7dconfig/db.js
const mongoose = require('mongoose');
const connectDB = async () => { try { await mongoose.connect(process.env.MONGODB_URI); console.log('MongoDB connected'); } catch (err) { console.error('Database connection error:' err); process.exit(1); }};
module.exports = connectDB;2. Utility Functions
utils/AppError.js
class AppError extends Error { constructor(message statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this this.constructor); }}
module.exports = AppError;utils/catchAsync.js
const catchAsync = (fn) => { return (req res next) => { fn(req res next).catch(next); };};
module.exports = catchAsync;3. Models
models/User.js
const mongoose = require('mongoose');const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({ name: { type: String required: true trim: true } email: { type: String required: true unique: true lowercase: true match: [/^\S+@\S+\.\S+$/ 'Please enter a valid email'] } password: { type: String required: true minlength: 6 select: false } role: { type: String enum: ['user' 'admin'] default: 'user' }} { timestamps: true });
<!-- Hash password before saving -->userSchema.pre('save' async function(next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password 12); next();});
<!-- Compare password method -->userSchema.methods.comparePassword = async function(candidatePassword) { return await bcrypt.compare(candidatePassword this.password);};
module.exports = mongoose.model('User' userSchema);models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({ description: { type: String required: true trim: true } completed: { type: Boolean default: false } priority: { type: String enum: ['low' 'medium' 'high'] default: 'medium' } dueDate: { type: Date } user: { type: mongoose.Schema.Types.ObjectId ref: 'User' required: true }} { timestamps: true });
module.exports = mongoose.model('Task' taskSchema);4. Authentication Middleware
middleware/auth.js
const jwt = require('jsonwebtoken');const User = require('../models/User');const AppError = require('../utils/AppError');const catchAsync = require('../utils/catchAsync');
const protect = catchAsync(async (req res next) => { let token; if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) { token = req.headers.authorization.split(' ')[1]; } if (!token) { return next(new AppError('You are not logged in. Please log in to access this resource.' 401)); } <!-- Verify token --> const decoded = jwt.verify(token process.env.JWT_SECRET); <!-- Check if user still exists --> const user = await User.findById(decoded.id).select('-password'); if (!user) { return next(new AppError('The user belonging to this token no longer exists.' 401)); } req.user = user; next();});
const restrictTo = (...roles) => { return (req res next) => { if (!roles.includes(req.user.role)) { return next(new AppError('You do not have permission to perform this action' 403)); } next(); };};
module.exports = { protect restrictTo };5. Controllers
controllers/authController.js
const jwt = require('jsonwebtoken');const User = require('../models/User');const AppError = require('../utils/AppError');const catchAsync = require('../utils/catchAsync');
const signToken = (id) => { return jwt.sign({ id } process.env.JWT_SECRET { expiresIn: process.env.JWT_EXPIRES_IN });};
const createSendToken = (user statusCode res) => { const token = signToken(user._id); <!-- Remove password from output --> user.password = undefined; res.status(statusCode).json({ status: 'success' token data: { user } });};
const register = catchAsync(async (req res next) => { const { name email password } = req.body; <!-- Check if user exists --> const existingUser = await User.findOne({ email }); if (existingUser) { return next(new AppError('User already exists with this email' 400)); } <!-- Create user --> const user = await User.create({ name email password }); createSendToken(user 201 res);});
const login = catchAsync(async (req res next) => { const { email password } = req.body; if (!email || !password) { return next(new AppError('Please provide email and password' 400)); } <!-- Get user with password --> const user = await User.findOne({ email }).select('+password'); if (!user || !(await user.comparePassword(password))) { return next(new AppError('Incorrect email or password' 401)); } createSendToken(user 200 res);});
const getMe = (req res) => { res.status(200).json({ status: 'success' data: { user: req.user } });};
module.exports = { register login getMe };controllers/taskController.js
const Task = require('../models/Task');const AppError = require('../utils/AppError');const catchAsync = require('../utils/catchAsync');
const getAllTasks = catchAsync(async (req res next) => { const tasks = await Task.find({ user: req.user._id }).sort('-createdAt'); res.status(200).json({ status: 'success' results: tasks.length data: { tasks } });});
const getTask = catchAsync(async (req res next) => { const task = await Task.findOne({ _id: req.params.id user: req.user._id }); if (!task) { return next(new AppError('No task found with that ID' 404)); } res.status(200).json({ status: 'success' data: { task } });});
const createTask = catchAsync(async (req res next) => { const task = await Task.create({ ...req.body user: req.user._id }); res.status(201).json({ status: 'success' data: { task } });});
const updateTask = catchAsync(async (req res next) => { const task = await Task.findOneAndUpdate( { _id: req.params.id user: req.user._id } req.body { new: true runValidators: true } ); if (!task) { return next(new AppError('No task found with that ID' 404)); } res.status(200).json({ status: 'success' data: { task } });});
const deleteTask = catchAsync(async (req res next) => { const task = await Task.findOneAndDelete({ _id: req.params.id user: req.user._id }); if (!task) { return next(new AppError('No task found with that ID' 404)); } res.status(204).json({ status: 'success' data: null });});
module.exports = { getAllTasks getTask createTask updateTask deleteTask};6. Routes
routes/authRoutes.js
const express = require('express');const authController = require('../controllers/authController');const { protect } = require('../middleware/auth');
const router = express.Router();
router.post('/register' authController.register);router.post('/login' authController.login);router.get('/me' protect authController.getMe);
module.exports = router;routes/taskRoutes.js
const express = require('express');const taskController = require('../controllers/taskController');const { protect } = require('../middleware/auth');
const router = express.Router();
// All task routes are protectedrouter.use(protect);
router.route('/') .get(taskController.getAllTasks) .post(taskController.createTask);
router.route('/:id') .get(taskController.getTask) .patch(taskController.updateTask) .delete(taskController.deleteTask);
module.exports = router;7. Error Handler Middleware
middleware/errorHandler.js
const AppError = require('../utils/AppError');
const handleCastErrorDB = (err) => { const message = `Invalid ${err.path}: ${err.value}.`; return new AppError(message, 400);};
const handleDuplicateFieldsDB = (err) => { const value = err.errmsg.match(/(["'])(?:(?=(\\?))\2.)*?\1/)[0]; const message = `Duplicate field value: ${value}. Please use another value!`; return new AppError(message, 400);};
const handleValidationErrorDB = (err) => { const errors = Object.values(err.errors).map(el => el.message); const message = `Invalid input data. ${errors.join('. ')}`; return new AppError(message, 400);};
const sendErrorDev = (err, res) => { res.status(err.statusCode).json({ status: err.statusCode, error: err, message: err.message, stack: err.stack });};
const sendErrorProd = (err, res) => { // Operational, trusted error: send message to client if (err.isOperational) { res.status(err.statusCode).json({ status: err.statusCode, message: err.message }); } else { // Programming or other unknown error: don't leak details console.error('ERROR ', err); res.status(500).json({ status: 'error', message: 'Something went wrong!' }); }};
module.exports = (err, req, res, next) => { err.statusCode = err.statusCode || 500; if (process.env.NODE_ENV === 'development') { sendErrorDev(err, res); } else { let error = { ...err }; error.message = err.message; if (error.name === 'CastError') error = handleCastErrorDB(error); if (error.code === 11000) error = handleDuplicateFieldsDB(error); if (error.name === 'ValidationError') error = handleValidationErrorDB(error); sendErrorProd(error, res); }};8. Main App File
app.js
const express = require('express');const rateLimit = require('express-rate-limit');const helmet = require('helmet');const authRoutes = require('./routes/authRoutes');const taskRoutes = require('./routes/taskRoutes');const errorHandler = require('./middleware/errorHandler');const AppError = require('./utils/AppError');
const app = express();
// Security middlewareapp.use(helmet());
// Rate limitingconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100});app.use('/api', limiter);
// Body parserapp.use(express.json({ limit: '10kb' }));
// Routesapp.use('/api/auth', authRoutes);app.use('/api/tasks', taskRoutes);
// 404 handlerapp.all('*', (req, res, next) => { next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));});
// Global error handlerapp.use(errorHandler);
module.exports = app;server.js
require('dotenv').config();const connectDB = require('./config/db');const app = require('./app');
// Connect to databaseconnectDB();
const port = process.env.PORT || 3000;
const server = app.listen(port, () => { console.log(`Server running on port ${port}`);});
// Handle unhandled promise rejectionsprocess.on('unhandledRejection', (err) => { console.log('UNHANDLED REJECTION! Shutting down...'); console.log(err.name, err.message); server.close(() => { process.exit(1); });});Two Minute Drill
- This project combines authentication, CRUD operations, and security best practices
- Use JWT for stateless authentication
- Always scope queries to the authenticated user (user-specific data)
- Organize code with MVC pattern: Models, Controllers, Routes
- Implement proper error handling and use catchAsync to avoid try/catch repetition
Need more clarification?
Drop us an email at career@quipoinfotech.com
