Chapter 7: Build Backend Projects

It's time to build! The following projects are designed to be "headless" – meaning they focus entirely on the backend logic (APIs, databases, services) without requiring a frontend. You can test each of these projects using an API client like Postman or Insomnia.

For each project, create a new folder, run `npm init -y`, install the required packages, create a `server.js` file, and paste the code. Let's get started!

1. Basic Blog API (CRUD)

A fundamental REST API for creating, reading, updating, and deleting blog posts. This is a core skill for any backend developer.

  • Skills Practiced: REST API principles, Express routing, Mongoose Schemas & Models, CRUD operations, `async/await`.

Core Logic:

  1. Define a Mongoose schema for a `Post` with `title` and `content`.
  2. Create a `Post` model from the schema.
  3. Set up Express routes for `GET /posts`, `GET /posts/:id`, `POST /posts`, `PATCH /posts/:id`, and `DELETE /posts/:id`.
  4. Connect each route to the corresponding Mongoose method (`find`, `findById`, `save`, `findByIdAndUpdate`, `findByIdAndDelete`).

Example Code (`server.js`):

// Setup: npm install express mongoose dotenv
// Create a .env file with your MONGO_URI
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json()); // Middleware to parse JSON

// --- Mongoose Schema & Model ---
const postSchema = new mongoose.Schema({
    title: { type: String, required: true },
    content: { type: String, required: true },
    createdAt: { type: Date, default: Date.now }
});
const Post = mongoose.model('Post', postSchema);

// --- API Routes ---
// CREATE a new post
app.post('/posts', async (req, res) => {
    try {
        const newPost = new Post({ title: req.body.title, content: req.body.content });
        const savedPost = await newPost.save();
        res.status(201).json(savedPost);
    } catch (err) { res.status(400).json({ message: err.message }); }
});

// READ all posts
app.get('/posts', async (req, res) => {
    try {
        const posts = await Post.find();
        res.json(posts);
    } catch (err) { res.status(500).json({ message: err.message }); }
});

// READ a single post by ID
app.get('/posts/:id', async (req, res) => {
    try {
        const post = await Post.findById(req.params.id);
        if (!post) return res.status(404).json({ message: 'Post not found' });
        res.json(post);
    } catch (err) { res.status(500).json({ message: err.message }); }
});

// UPDATE a post by ID
app.patch('/posts/:id', async (req, res) => {
    try {
        const updatedPost = await Post.findByIdAndUpdate(req.params.id, req.body, { new: true });
        if (!updatedPost) return res.status(404).json({ message: 'Post not found' });
        res.json(updatedPost);
    } catch (err) { res.status(400).json({ message: err.message }); }
});

// DELETE a post by ID
app.delete('/posts/:id', async (req, res) => {
    try {
        const deletedPost = await Post.findByIdAndDelete(req.params.id);
        if (!deletedPost) return res.status(404).json({ message: 'Post not found' });
        res.json({ message: 'Post deleted' });
    } catch (err) { res.status(500).json({ message: err.message }); }
});


// --- Database Connection & Server Start ---
const PORT = process.env.PORT || 3000;
mongoose.connect(process.env.MONGO_URI)
    .then(() => {
        console.log('MongoDB Connected...');
        app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
    })
    .catch(err => console.log(err));

2. URL Shortener

An API that takes a long URL and returns a short, unique code. When someone visits the short URL, they are redirected to the original long URL.

  • Skills Practiced: Routing with parameters, Database interaction, Generating unique strings.

Core Logic:

  1. Create a schema with `originalUrl` and `shortCode`.
  2. `POST /shorten`: When a long URL is received, generate a unique short code, save the pair to the database, and return the short URL.
  3. `GET /:shortCode`: When a request comes to this route, find the document with the matching `shortCode` in the database. If found, redirect the user to the `originalUrl`. If not, return a 404 error.

Example Code (`server.js`):

// Setup: npm install express mongoose nanoid dotenv
// Create a .env file with your MONGO_URI
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const { nanoid } = require('nanoid'); // For generating short codes
const app = express();
app.use(express.json());

// --- Mongoose Schema & Model ---
const urlSchema = new mongoose.Schema({
    originalUrl: { type: String, required: true },
    shortCode: { type: String, required: true, unique: true, default: () => nanoid(7) }
});
const Url = mongoose.model('Url', urlSchema);

// --- API Routes ---
// CREATE a short URL
app.post('/shorten', async (req, res) => {
    try {
        const { originalUrl } = req.body;
        // Optional: Check if URL already exists
        let url = await Url.findOne({ originalUrl });
        if (url) {
            res.json(url);
        } else {
            const newUrl = new Url({ originalUrl });
            await newUrl.save();
            res.status(201).json(newUrl);
        }
    } catch (err) { res.status(500).json({ message: 'Server error' }); }
});

// REDIRECT to original URL
app.get('/:shortCode', async (req, res) => {
    try {
        const url = await Url.findOne({ shortCode: req.params.shortCode });
        if (url) {
            return res.redirect(url.originalUrl);
        } else {
            return res.status(404).json('No URL found');
        }
    } catch (err) { res.status(500).json('Server error'); }
});


// --- Database Connection & Server Start ---
const PORT = process.env.PORT || 3000;
mongoose.connect(process.env.MONGO_URI)
    .then(() => app.listen(PORT, () => console.log(`Server running on port ${PORT}`)))
    .catch(err => console.log(err));

3. User Authentication API

The foundation of any application with user accounts. This API will handle user signup and login, returning a JWT for accessing protected routes.

  • Skills Practiced: Password hashing (`bcrypt`), JWT creation/verification, secure API design, middleware.

Core Logic:

  1. `POST /signup`: Hash the user's password using `bcrypt` and save the new user to the database.
  2. `POST /login`: Find the user by email. Use `bcrypt.compare()` to check if the submitted password matches the stored hash.
  3. If the login is successful, generate a JWT containing the user's ID and sign it with a secret key. Return the token.
  4. Create a protected route (e.g., `GET /profile`) and a middleware function that checks for a valid JWT in the `Authorization` header.

Example Code (`server.js`):

// Setup: npm install express mongoose bcrypt jsonwebtoken dotenv
// Create .env with MONGO_URI and a strong JWT_SECRET
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());

// --- User Schema & Model ---
const userSchema = new mongoose.Schema({
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true }
});
const User = mongoose.model('User', userSchema);

// --- Middleware to protect routes ---
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    if (token == null) return res.sendStatus(401);

    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
    });
};

// --- Auth Routes ---
// SIGNUP
app.post('/signup', async (req, res) => {
    try {
        const hashedPassword = await bcrypt.hash(req.body.password, 10);
        const user = new User({ email: req.body.email, password: hashedPassword });
        await user.save();
        res.status(201).send('User created');
    } catch { res.status(500).send(); }
});

// LOGIN
app.post('/login', async (req, res) => {
    const user = await User.findOne({ email: req.body.email });
    if (user == null) return res.status(400).send('Cannot find user');
    try {
        if (await bcrypt.compare(req.body.password, user.password)) {
            const payload = { userId: user._id };
            const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
            res.json({ accessToken: accessToken });
        } else {
            res.send('Not Allowed');
        }
    } catch { res.status(500).send(); }
});

// PROTECTED ROUTE
app.get('/profile', authenticateToken, (req, res) => {
    // req.user is available from the middleware
    res.json({ message: `Welcome user with ID: ${req.user.userId}` });
});


// --- DB & Server ---
const PORT = process.env.PORT || 3000;
mongoose.connect(process.env.MONGO_URI)
    .then(() => app.listen(PORT, () => console.log(`Server running on port ${PORT}`)))
    .catch(err => console.log(err));

4. Weather API Proxy

An API that acts as a middleman. Your frontend calls *your* server, and your server securely calls the external weather API, hiding your secret API key.

  • Skills Practiced: Calling external APIs from Node.js (`axios`), securing API keys with environment variables.

Example Code (`server.js`):

// Setup: npm install express axios dotenv
// Create .env with a WEATHER_API_KEY from OpenWeatherMap
require('dotenv').config();
const express = require('express');
const axios = require('axios'); // For making HTTP requests
const app = express();

app.get('/weather', async (req, res) => {
    try {
        const city = req.query.city;
        if (!city) return res.status(400).json({ message: 'City query parameter is required' });

        const apiKey = process.env.WEATHER_API_KEY;
        const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;
        
        const weatherResponse = await axios.get(apiUrl);
        res.json(weatherResponse.data);

    } catch (error) {
        if (error.response) { // The request was made and the server responded with a status code
            res.status(error.response.status).json({ message: error.response.data.message });
        } else {
            res.status(500).json({ message: 'Server error' });
        }
    }
});

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

5. Task Scheduler (Cron Job)

A script that automatically runs a specific task on a schedule (e.g., send a report every morning, clean up temporary files once a week).

  • Skills Practiced: Scheduling tasks (`node-cron`), running background processes.

Example Code (`server.js`):

// Setup: npm install node-cron
const cron = require('node-cron');
const express = require('express');
const app = express();

console.log('Scheduler started. Waiting for tasks...');

// Schedule a task to run every minute
cron.schedule('* * * * *', () => {
  console.log(`Task is running every minute - ${new Date()}`);
  // Example task: Fetch data, send an email, perform cleanup, etc.
});

// Schedule a task to run at 8 AM every day
cron.schedule('0 8 * * *', () => {
    console.log('Running daily report at 8:00 AM');
});

// Keep the server running so the scheduler stays active
app.get('/', (req, res) => res.send('Cron scheduler is active. Check the console!'));
app.listen(3000);

6. File Upload API

An endpoint that can accept file uploads (like images) and save them to a directory on the server.

  • Skills Practiced: Handling `multipart/form-data`, using middleware (`multer`), working with the file system.

Example Code (`server.js`):

// Setup: npm install express multer
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();

// Set up storage for Multer
const storage = multer.diskStorage({
    destination: './uploads/',
    filename: function(req, file, cb){
      cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname));
    }
});
const upload = multer({ storage: storage });

// Route to handle single file upload
app.post('/upload', upload.single('myFile'), (req, res) => {
    if (!req.file) {
        return res.status(400).send('No file uploaded.');
    }
    console.log(req.file); // Information about the uploaded file
    res.send(`File uploaded successfully: ${req.file.path}`);
});

// Test with Postman: Send a POST request to /upload. 
// In the Body tab, select 'form-data', enter 'myFile' as the key,
// change the type to 'File', and select a file from your computer.

app.listen(3000, () => console.log('Server started on port 3000'));

7. Real-time Chat Server (Simple)

A basic WebSocket server that broadcasts any message it receives to all connected clients.

  • Skills Practiced: WebSockets (`ws` library), event-driven programming, managing connections.

Example Code (`server.js`):

// Setup: npm install ws express
const express = require('express');
const http = require('http');
const WebSocket = require('ws');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

wss.on('connection', ws => {
    console.log('Client connected');
    
    ws.on('message', message => {
        console.log(`Received message: ${message}`);
        // Broadcast the message to all clients
        wss.clients.forEach(client => {
            if (client !== ws && client.readyState === WebSocket.OPEN) {
                client.send(String(message)); // Ensure it's a string
            }
        });
    });

    ws.on('close', () => {
        console.log('Client disconnected');
    });
});

server.listen(8080, () => {
    console.log('WebSocket server started on port 8080');
});

// To test, use a simple WebSocket client tool or a small HTML file.

8. API Rate Limiter

Protect your API from abuse by limiting how many requests a user can make in a given time period.

  • Skills Practiced: Express middleware, security concepts.

Example Code (`server.js`):

// Setup: npm install express express-rate-limit
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();

const limiter = rateLimit({
	windowMs: 15 * 60 * 1000, // 15 minutes
	max: 100, // Limit each IP to 100 requests per windowMs
	standardHeaders: true, 
	legacyHeaders: false, 
    message: 'Too many requests from this IP, please try again after 15 minutes'
});

// Apply the rate limiting middleware to all requests
app.use(limiter);

// Your API routes
app.get('/', (req, res) => {
    res.send('This is a rate-limited API!');
});

app.get('/api/data', (req, res) => {
    res.json({ data: 'Some important data' });
});

app.listen(3000, () => console.log('Server with rate limiter running on port 3000'));

9. Simple E-commerce API

Endpoints to list products and view a single product. This is a great way to practice data modeling.

  • Skills Practiced: Advanced CRUD, data modeling in Mongoose, routing with parameters.

Example Code (`server.js`):

// Setup: npm install express mongoose dotenv
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express(); app.use(express.json());

const productSchema = new mongoose.Schema({
    name: { type: String, required: true },
    price: { type: Number, required: true, min: 0 },
    description: String,
    inStock: { type: Boolean, default: true }
});
const Product = mongoose.model('Product', productSchema);

// CREATE a new product
app.post('/products', async (req, res) => {
    try {
        const product = new Product(req.body);
        await product.save();
        res.status(201).json(product);
    } catch (err) { res.status(400).json({ message: err.message }); }
});

// GET all products
app.get('/products', async (req, res) => {
    try {
        const products = await Product.find();
        res.json(products);
    } catch (err) { res.status(500).json({ message: err.message }); }
});

// GET a single product by ID
app.get('/products/:id', async (req, res) => {
    try {
        const product = await Product.findById(req.params.id);
        if (!product) return res.status(404).json({ message: 'Product not found' });
        res.json(product);
    } catch (err) { res.status(500).json({ message: err.message }); }
});

const PORT = 3000;
mongoose.connect(process.env.MONGO_URI)
    .then(() => app.listen(PORT, () => console.log(`Server running on port ${PORT}`)))
    .catch(err => console.log(err));

10. Basic Web Scraper

A script that fetches a webpage, parses its HTML, and extracts specific data (e.g., all the headlines from a news site).

  • Skills Practiced: HTTP requests (`axios`), HTML parsing (`cheerio`).

Example Code (`scraper.js` - run with `node scraper.js`):

// Setup: npm install axios cheerio
const axios = require('axios');
const cheerio = require('cheerio');

// URL to scrape (use a simple site for example)
const url = 'https://news.ycombinator.com';

async function scrapeData() {
    try {
        const { data } = await axios.get(url);
        const $ = cheerio.load(data); // Load HTML into Cheerio
        
        const headlines = [];
        // Use a CSS selector to find all headline elements
        $('.titleline > a').each((index, element) => {
            headlines.push($(element).text());
        });

        console.log('Scraped Headlines:');
        console.log(headlines);

    } catch (error) {
        console.error('Error scraping data:', error.message);
    }
}

scrapeData();