Lesson 20 of 20

Final Project: Build a REST API

Project Setup and Configuration

In this final project, you will build a complete REST API with authentication, database integration, validation, and error handling. This brings together everything you have learned in the previous 19 lessons.

We will build a Task Manager API where users can register, log in, and manage their personal tasks. The API will use Express, MongoDB with Mongoose, JWT authentication, and proper error handling.

Example
// Initialize project
// mkdir task-api && cd task-api
// npm init -y
// npm install express mongoose bcrypt jsonwebtoken dotenv helmet cors
// npm install -D nodemon

// .env
// PORT=3000
// DATABASE_URL=mongodb://localhost:27017/taskmanager
// JWT_SECRET=your-secret-key-here

// server.js — Entry point
require('dotenv').config();
const mongoose = require('mongoose');
const app = require('./src/app');

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

mongoose.connect(process.env.DATABASE_URL)
  .then(() => {
    console.log('Connected to MongoDB');
    app.listen(PORT, () => {
      console.log(`Task API running on http://localhost:${PORT}`);
    });
  })
  .catch(err => {
    console.error('Database connection failed:', err.message);
    process.exit(1);
  });

// src/app.js — Express app setup
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const authRoutes = require('./routes/authRoutes');
const taskRoutes = require('./routes/taskRoutes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json());

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

app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.use(errorHandler);

module.exports = app;
  • express — Web framework for routing and middleware
  • mongoose — MongoDB ODM for schema and data modeling
  • bcrypt — Secure password hashing
  • jsonwebtoken — JWT-based authentication
  • dotenv — Load environment variables from .env
  • helmet + cors — Security middleware
Notes
  • This project structure separates concerns: server.js handles startup and database connection, app.js sets up Express with middleware and routes.

Models, Auth Routes, and Task Routes

The User model stores credentials with hashed passwords. The Task model links tasks to users. Auth routes handle registration and login. Task routes provide full CRUD with authentication.

Each task belongs to a user (via the owner field). The auth middleware ensures users can only access their own tasks.

Example
// src/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 },
  password: { type: String, required: true, minlength: 6 }
});

userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

module.exports = mongoose.model('User', userSchema);

// src/models/Task.js
const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
  title: { type: String, required: true, trim: true },
  description: { type: String, default: '' },
  completed: { type: Boolean, default: false },
  priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' },
  owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
}, { timestamps: true });

module.exports = mongoose.model('Task', taskSchema);

// src/routes/authRoutes.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('../models/User');
const router = express.Router();

router.post('/register', async (req, res, next) => {
  try {
    const { name, email, password } = req.body;
    const user = await User.create({ name, email, password });
    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
    res.status(201).json({ token, user: { id: user._id, name, email } });
  } catch (err) {
    if (err.code === 11000) return res.status(400).json({ error: 'Email already registered' });
    next(err);
  }
});

router.post('/login', async (req, res, next) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ email });
    if (!user || !(await bcrypt.compare(password, user.password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
    res.json({ token, user: { id: user._id, name: user.name, email } });
  } catch (err) { next(err); }
});

module.exports = router;

// src/routes/taskRoutes.js
const express = require('express');
const Task = require('../models/Task');
const auth = require('../middleware/auth');
const router = express.Router();

router.use(auth); // All task routes require authentication

router.get('/', async (req, res, next) => {
  try {
    const { completed, priority, sort = '-createdAt' } = req.query;
    const filter = { owner: req.user.id };
    if (completed !== undefined) filter.completed = completed === 'true';
    if (priority) filter.priority = priority;
    const tasks = await Task.find(filter).sort(sort);
    res.json(tasks);
  } catch (err) { next(err); }
});

router.post('/', async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, owner: req.user.id });
    res.status(201).json(task);
  } catch (err) { next(err); }
});

router.put('/:id', async (req, res, next) => {
  try {
    const task = await Task.findOneAndUpdate(
      { _id: req.params.id, owner: req.user.id },
      req.body,
      { new: true, runValidators: true }
    );
    if (!task) return res.status(404).json({ error: 'Task not found' });
    res.json(task);
  } catch (err) { next(err); }
});

router.delete('/:id', async (req, res, next) => {
  try {
    const task = await Task.findOneAndDelete({ _id: req.params.id, owner: req.user.id });
    if (!task) return res.status(404).json({ error: 'Task not found' });
    res.status(204).send();
  } catch (err) { next(err); }
});

module.exports = router;
Notes
  • The User model uses a pre-save hook to automatically hash the password before saving. The Task routes filter by owner to ensure users only access their own tasks.

Auth Middleware and Error Handler

The auth middleware verifies the JWT token and attaches the user to the request. The error handler middleware catches all errors and returns consistent JSON responses.

With these pieces in place, you have a complete, production-ready REST API pattern. Congratulations on completing the Node.js course!

Example
// src/middleware/auth.js
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  try {
    const token = header.split(' ')[1];
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

module.exports = authenticate;

// src/middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  console.error(`[${new Date().toISOString()}] ${err.stack}`);

  if (err.name === 'ValidationError') {
    const messages = Object.values(err.errors).map(e => e.message);
    return res.status(400).json({ error: 'Validation failed', details: messages });
  }
  if (err.name === 'CastError') {
    return res.status(400).json({ error: 'Invalid ID format' });
  }
  if (err.code === 11000) {
    return res.status(400).json({ error: 'Duplicate value' });
  }

  res.status(err.statusCode || 500).json({
    error: err.message || 'Internal Server Error'
  });
}

module.exports = errorHandler;

// --- Testing your API with curl ---
// Register:
// curl -X POST http://localhost:3000/api/auth/register \
//   -H "Content-Type: application/json" \
//   -d '{"name":"Alice","email":"alice@test.com","password":"pass123"}'
//
// Login:
// curl -X POST http://localhost:3000/api/auth/login \
//   -H "Content-Type: application/json" \
//   -d '{"email":"alice@test.com","password":"pass123"}'
//
// Create task (use token from login):
// curl -X POST http://localhost:3000/api/tasks \
//   -H "Content-Type: application/json" \
//   -H "Authorization: Bearer YOUR_TOKEN" \
//   -d '{"title":"Learn Node.js","priority":"high"}'
Notes
  • Congratulations! You have built a complete REST API with Express, MongoDB, JWT authentication, input validation, and error handling. This pattern scales to real-world production applications.