Month 2, Week 3

Building a Full CRUD API with Express

From Theory to a Working In-Memory Application

Module 1: Architecting a RESTful API

The Blueprint for Modern Web Services

What is REST?

REpresentational State Transfer is an architectural style for designing networked applications. It's not a protocol or a standard; it's a set of constraints.

The goal is to create APIs that are simple, scalable, and predictable.

Core REST Principles

  • Client-Server Architecture: The client (e.g., a web browser or mobile app) and the server are separate. They communicate over a network (HTTP).
  • Statelessness: Each request from a client to a server must contain all the information needed to understand and process the request. The server does not store any client session state.
  • Uniform Interface: This is the key constraint. It simplifies the architecture and makes the API predictable. It has four parts...

Uniform Interface: CRUD & HTTP Verbs

We model our data as resources (e.g., users, products, posts). We use standard HTTP verbs to perform actions on these resources.

Operation CRUD HTTP Verb Example Route Description
Create Create POST `/users` Create a new user.
Read Read GET `/users`, `/users/:id` Retrieve a list of users or a single user.
Update Update PUT / PATCH `/users/:id` Update a user's information.
Delete Delete DELETE `/users/:id` Delete a user.

Resource Naming Conventions

Good URLs are predictable and easy to understand.

  • Use plural nouns for resources (e.g., `/posts`, not `/post`).
  • Use path parameters for specific resources (e.g., `/posts/123`).
  • Use query parameters for filtering/sorting (e.g., `/posts?status=published`).
  • Do not use verbs in your URLs. The HTTP method *is* the verb.

Good: `GET /users/42/comments`

Bad: `POST /getUserComments?userId=42`

HTTP Status Codes: The Server's Response

Status codes are a standard way for the server to tell the client the outcome of its request.

  • 2xx (Success):
    • `200 OK`: Standard success for `GET` and `PATCH`.
    • `201 Created`: Success for `POST`, a new resource was created.
    • `204 No Content`: Success for `DELETE`, nothing to send back.
  • 4xx (Client Error):
    • `400 Bad Request`: Invalid data from the client (e.g., missing email).
    • `404 Not Found`: The requested resource does not exist.
  • 5xx (Server Error):
    • `500 Internal Server Error`: Something went wrong on the server.

Mid-Lecture Knowledge Check

Module 2: Building the API - Step by Step

From `npm init` to a Working Server

Step 1: Project Structure

We will use Separation of Concerns to keep our code organized.


                        /my-api
                        ├── node_modules/
                        ├── data/
                        │   └── users.js      (Our in-memory database)
                        ├── controllers/
                        │   └── userController.js (The logic for each route)
                        ├── routes/
                        │   └── userRoutes.js (The Express Router)
                        ├── app.js            (The main server file)
                        └── package.json
                    

Step 2: The In-Memory Datastore

To isolate our API logic from database complexity, we'll start with a simple array.

`data/users.js`


                        let users = [
                          { id: 1, name: 'Alex Architect', email: 'alex@example.com' },
                          { id: 2, name: 'Jane Developer', email: 'jane@example.com' }
                        ];

                        module.exports = users;
                    

Step 3: The Controllers (The Logic)

Controllers contain the actual logic for handling a request.

`controllers/userController.js`


                        let users = require('../data/users');
                        let nextId = 3;

                        // GET /users
                        const getAllUsers = (req, res) => {
                          res.json(users);
                        };

                        // GET /users/:id
                        const getUserById = (req, res) => {
                          const user = users.find(u => u.id === parseInt(req.params.id));
                          if (!user) {
                            return res.status(404).send('User not found');
                          }
                          res.json(user);
                        };

                        // ... other functions for create, update, delete ...

                        module.exports = { getAllUsers, getUserById /*, ... */ };
                    

Step 4: The Router (The Endpoints)

The router connects an HTTP verb and a URL path to a controller function.

`routes/userRoutes.js`


                        const express = require('express');
                        const router = express.Router();
                        const userController = require('../controllers/userController');

                        router.get('/', userController.getAllUsers);
                        router.get('/:id', userController.getUserById);
                        // router.post('/', userController.createUser);
                        // router.patch('/:id', userController.updateUser);
                        // router.delete('/:id', userController.deleteUser);

                        module.exports = router;
                     

Step 5: The App (Putting It All Together)

`app.js`


                        const express = require('express');
                        const app = express();
                        const userRoutes = require('./routes/userRoutes');

                        const PORT = 3000;

                        // Middleware to parse JSON bodies
                        app.use(express.json());

                        // Mount the router
                        app.use('/users', userRoutes);

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

Module 3: Implementing All CRUD Operations

Bringing the API to Life

Create (POST)

Read the new user data from `req.body`, add it to the array, and return the new user with a `201 Created` status.


                        // In userController.js
                        const createUser = (req, res) => {
                          if (!req.body.name || !req.body.email) {
                            return res.status(400).send('Name and email are required.');
                          }
                          const newUser = {
                            id: nextId++,
                            name: req.body.name,
                            email: req.body.email
                          };
                          users.push(newUser);
                          res.status(201).json(newUser);
                        };
                     

Update (PATCH)

Find the user by their ID from `req.params`, update their data from `req.body`, and return the updated user.


                        // In userController.js
                        const updateUser = (req, res) => {
                            const user = users.find(u => u.id === parseInt(req.params.id));
                            if (!user) return res.status(404).send('User not found');

                            // Update properties if they exist in the request body
                            if (req.body.name) user.name = req.body.name;
                            if (req.body.email) user.email = req.body.email;

                            res.json(user);
                        };
                     

Delete (DELETE)

Find the user by their ID from `req.params`, remove them from the array, and return a `204 No Content` status.


                        // In userController.js
                        const deleteUser = (req, res) => {
                            const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
                            if (userIndex === -1) return res.status(404).send('User not found');

                            users.splice(userIndex, 1);
                            
                            res.status(204).send();
                        };
                      

In-Class Practical Exercise

Building the `posts` Resource

You will be given a project with a working `users` API. Your task is to architect and implement a new, complete CRUD API for a `posts` resource from scratch.

  1. Create a new in-memory datastore: `data/posts.js`. Each post should have an `id`, `title`, `content`, and `userId`.
  2. Create a new controller file: `controllers/postController.js`.
  3. Implement all five controller functions: `getAllPosts`, `getPostById`, `createPost`, `updatePost`, and `deletePost`.
  4. Create a new router file: `routes/postRoutes.js`.
  5. Define all five RESTful routes in your router and connect them to the controller functions.
  6. Mount your new `postRoutes` router in `app.js` at the path `/posts`.
  7. Use a tool like Postman or Insomnia to test every single one of your new endpoints.

Final Knowledge Check