Loading

Quipoin Menu

Learn • Practice • Grow

express-js / Build RESTful API (CRUD + Auth)
tutorial

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-api
cd task-manager-api
npm init -y
npm install express mongoose bcrypt jsonwebtoken dotenv express-rate-limit helmet
npm install --save-dev nodemon

Folder 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.js

1. Configuration Files

.env
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/task-manager
JWT_SECRET=your-super-secret-jwt-key-change-this
JWT_EXPIRES_IN=7d

config/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 protected
router.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 middleware
app.use(helmet());

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100
});
app.use('/api', limiter);

// Body parser
app.use(express.json({ limit: '10kb' }));

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/tasks', taskRoutes);

// 404 handler
app.all('*', (req, res, next) => {
  next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
});

// Global error handler
app.use(errorHandler);

module.exports = app;

server.js
require('dotenv').config();
const connectDB = require('./config/db');
const app = require('./app');

// Connect to database
connectDB();

const port = process.env.PORT || 3000;

const server = app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

// Handle unhandled promise rejections
process.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