COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

REST (Representational State Transfer)

REST is an architectural style for designing networked applications, particularly web APIs. It uses HTTP methods to implement CRUD operations in a standardized, stateless, and scalable way. RESTful APIs treat everything as a resource that can be accessed and manipulated through standard HTTP verbs.

Core Principles

1. Resources and URLs

Everything in REST is a resource (users, products, orders, etc.). Each resource is identified by a unique URL. URLs should be nouns (resources), not verbs (actions).

// GOOD - Resource-based URLs (nouns)
GET    /users              // Get all users
GET    /users/123          // Get user with ID 123
GET    /users/123/posts    // Get posts by user 123
GET    /posts/456/comments // Get comments on post 456

// BAD - Action-based URLs (verbs)
GET    /getUsers           // Don't use verbs in URLs
POST   /createUser         // The HTTP method is the verb
GET    /deleteUser/123     // Use DELETE method instead
GET    /user/get/123       // Redundant

// Resource hierarchy shows relationships
/organizations/1/departments/2/employees/3

// This URL represents:
// - Organization with ID 1
// - Department with ID 2 (within that organization)
// - Employee with ID 3 (within that department)

2. HTTP Methods Map to CRUD

REST uses HTTP methods (verbs) to indicate the operation being performed on a resource. This provides a standardized way to implement CRUD.

// Express.js RESTful API
const express = require('express');
const app = express();
app.use(express.json());

// CREATE - POST creates new resource
app.post('/users', async (req, res) => {
    const newUser = {
        id: generateId(),
        name: req.body.name,
        email: req.body.email,
        createdAt: new Date()
    };

    await db.users.insert(newUser);

    // 201 Created with Location header pointing to new resource
    res.status(201)
       .location(`/users/${newUser.id}`)
       .json(newUser);
});

// READ - GET retrieves resources (safe, idempotent)
app.get('/users', async (req, res) => {
    const users = await db.users.findAll();
    res.json(users); // 200 OK
});

app.get('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);

    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    res.json(user); // 200 OK
});

// UPDATE - PUT replaces entire resource (idempotent)
app.put('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);

    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    // Replace entire resource
    const updatedUser = {
        id: req.params.id,
        name: req.body.name,
        email: req.body.email,
        updatedAt: new Date()
    };

    await db.users.update(req.params.id, updatedUser);
    res.json(updatedUser); // 200 OK
});

// UPDATE - PATCH partially updates resource (idempotent)
app.patch('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);

    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    // Merge changes
    const updatedUser = {
        ...user,
        ...req.body,
        updatedAt: new Date()
    };

    await db.users.update(req.params.id, updatedUser);
    res.json(updatedUser); // 200 OK
});

// DELETE - DELETE removes resource (idempotent)
app.delete('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);

    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    await db.users.delete(req.params.id);
    res.status(204).send(); // 204 No Content
});

3. Statelessness

Each request from client to server must contain all the information needed to understand and process the request. The server doesn't store client state between requests.

// STATELESS - Each request is independent
// Client includes authentication token with EVERY request
const response = await fetch('/api/users', {
    headers: {
        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
        'Content-Type': 'application/json'
    }
});

// Server doesn't remember previous requests
app.get('/users', authenticate, (req, res) => {
    // Extract user from token (not from server session)
    const currentUser = verifyToken(req.headers.authorization);

    // Process request with all needed information
    const users = db.users.findAll({ organizationId: currentUser.orgId });
    res.json(users);
});

// STATEFUL (NOT REST) - Server stores session
// Don't do this in RESTful APIs:
app.get('/login', (req, res) => {
    req.session.user = authenticatedUser; // Stores state on server
});

app.get('/users', (req, res) => {
    const user = req.session.user; // Relies on stored state
    // This violates statelessness
});

4. HTTP Status Codes

RESTful APIs use standard HTTP status codes to indicate the result of operations. This provides a consistent way to communicate success, errors, and edge cases.

// Success Codes (2xx)
res.status(200).json(data);        // OK - Request succeeded
res.status(201).json(newResource); // Created - Resource created
res.status(204).send();            // No Content - Success, no body

// Client Error Codes (4xx)
res.status(400).json({ error: 'Invalid input' });      // Bad Request
res.status(401).json({ error: 'Unauthorized' });       // Not authenticated
res.status(403).json({ error: 'Forbidden' });          // Not authorized
res.status(404).json({ error: 'Not found' });          // Resource doesn't exist
res.status(409).json({ error: 'Conflict' });           // Duplicate/conflict
res.status(422).json({ error: 'Validation failed' }); // Unprocessable

// Server Error Codes (5xx)
res.status(500).json({ error: 'Internal server error' });
res.status(503).json({ error: 'Service unavailable' });

// Example with proper status codes
app.post('/users', async (req, res) => {
    try {
        // Validate input
        if (!req.body.email) {
            return res.status(400).json({ error: 'Email is required' });
        }

        // Check for duplicates
        const existing = await db.users.findByEmail(req.body.email);
        if (existing) {
            return res.status(409).json({ error: 'Email already exists' });
        }

        // Create resource
        const newUser = await db.users.create(req.body);

        // Return 201 Created
        res.status(201)
           .location(`/users/${newUser.id}`)
           .json(newUser);

    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

REST in Practice

Query Parameters for Filtering and Pagination

Use query parameters to filter, sort, and paginate collections without creating new endpoints.

// Filtering
app.get('/users', async (req, res) => {
    const { role, status, search } = req.query;

    let query = db.users.createQuery();

    if (role) {
        query = query.where('role', role);
    }

    if (status) {
        query = query.where('status', status);
    }

    if (search) {
        query = query.where('name', 'LIKE', `%${search}%`);
    }

    const users = await query.execute();
    res.json(users);
});

// Usage:
// GET /users?role=admin
// GET /users?status=active&role=admin
// GET /users?search=alice

// Sorting
app.get('/users', async (req, res) => {
    const { sort = 'createdAt', order = 'desc' } = req.query;

    const users = await db.users
        .orderBy(sort, order)
        .execute();

    res.json(users);
});

// Usage:
// GET /users?sort=name&order=asc
// GET /users?sort=createdAt&order=desc

// Pagination
app.get('/users', async (req, res) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const offset = (page - 1) * limit;

    const [users, total] = await Promise.all([
        db.users.findAll({ limit, offset }),
        db.users.count()
    ]);

    res.json({
        data: users,
        pagination: {
            page,
            limit,
            total,
            totalPages: Math.ceil(total / limit)
        }
    });
});

// Usage:
// GET /users?page=1&limit=20
// GET /users?page=2&limit=20

Nested Resources

When resources have relationships, URLs can reflect the hierarchy. However, avoid deeply nested URLs (2 levels maximum is a good rule).

// User's posts
app.get('/users/:userId/posts', async (req, res) => {
    const posts = await db.posts.findByUserId(req.params.userId);
    res.json(posts);
});

// Specific post by a user
app.get('/users/:userId/posts/:postId', async (req, res) => {
    const post = await db.posts.findOne({
        id: req.params.postId,
        userId: req.params.userId
    });

    if (!post) {
        return res.status(404).json({ error: 'Post not found' });
    }

    res.json(post);
});

// AVOID deeply nested URLs (hard to maintain)
// BAD: GET /organizations/1/departments/2/teams/3/members/4/tasks/5

// BETTER: Use top-level resource with filters
// GET /tasks/5
// GET /tasks?memberId=4
// GET /tasks?teamId=3

Content Negotiation

REST supports multiple representations of the same resource. Clients can request different formats using the Accept header.

app.get('/users/:id', async (req, res) => {
    const user = await db.users.findById(req.params.id);

    if (!user) {
        return res.status(404).json({ error: 'Not found' });
    }

    // Check what format the client wants
    const acceptHeader = req.headers.accept;

    if (acceptHeader.includes('application/json')) {
        res.json(user);
    } else if (acceptHeader.includes('application/xml')) {
        res.type('application/xml');
        res.send(`
            <user>
                <id>${user.id}</id>
                <name>${user.name}</name>
                <email>${user.email}</email>
            </user>
        `);
    } else if (acceptHeader.includes('text/html')) {
        res.send(`
            <html>
                <body>
                    <h1>${user.name}</h1>
                    <p>Email: ${user.email}</p>
                </body>
            </html>
        `);
    } else {
        // Default to JSON
        res.json(user);
    }
});

// Client specifies format:
// Accept: application/json
// Accept: application/xml
// Accept: text/html

Client-Side REST Consumption

// Modern fetch API for REST
class UserAPI {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
    }

    async getHeaders() {
        return {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.getToken()}`
        };
    }

    // CREATE
    async createUser(userData) {
        const response = await fetch(`${this.baseUrl}/users`, {
            method: 'POST',
            headers: await this.getHeaders(),
            body: JSON.stringify(userData)
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        return response.json();
    }

    // READ (collection)
    async getUsers(filters = {}) {
        const params = new URLSearchParams(filters);
        const response = await fetch(`${this.baseUrl}/users?${params}`, {
            headers: await this.getHeaders()
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        return response.json();
    }

    // READ (single)
    async getUser(id) {
        const response = await fetch(`${this.baseUrl}/users/${id}`, {
            headers: await this.getHeaders()
        });

        if (response.status === 404) {
            return null;
        }

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        return response.json();
    }

    // UPDATE (full)
    async updateUser(id, userData) {
        const response = await fetch(`${this.baseUrl}/users/${id}`, {
            method: 'PUT',
            headers: await this.getHeaders(),
            body: JSON.stringify(userData)
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        return response.json();
    }

    // UPDATE (partial)
    async patchUser(id, partialData) {
        const response = await fetch(`${this.baseUrl}/users/${id}`, {
            method: 'PATCH',
            headers: await this.getHeaders(),
            body: JSON.stringify(partialData)
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        return response.json();
    }

    // DELETE
    async deleteUser(id) {
        const response = await fetch(`${this.baseUrl}/users/${id}`, {
            method: 'DELETE',
            headers: await this.getHeaders()
        });

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        // 204 No Content - no body to parse
        return response.status === 204 ? null : response.json();
    }

    getToken() {
        return localStorage.getItem('auth_token');
    }
}

// Usage
const api = new UserAPI('https://api.example.com');

// Create
const newUser = await api.createUser({
    name: 'Alice',
    email: 'alice@example.com'
});

// Read
const users = await api.getUsers({ role: 'admin', page: 1 });
const user = await api.getUser(123);

// Update
await api.updateUser(123, { name: 'Alice Johnson', email: 'alice@example.com' });
await api.patchUser(123, { name: 'Alice Johnson' }); // Only update name

// Delete
await api.deleteUser(123);

REST vs CRUD Comparison

Aspect CRUD REST
Concept Four data operations Architectural style for APIs
Create Create operation POST /resources
Read Read operation GET /resources or GET /resources/:id
Update Update operation PUT or PATCH /resources/:id
Delete Delete operation DELETE /resources/:id
Transport Can use any method Specifically uses HTTP
State Can be stateful Must be stateless
Resources Any data store URL-addressable resources

Benefits of REST

Challenges and Considerations

RESTful Design Best Practices

When REST Isn't Ideal

// Some operations don't fit CRUD/REST well:

// 1. Actions that aren't about a resource
POST /api/login           // Not really creating a "login" resource
POST /api/logout          // Not really creating a "logout" resource
POST /api/search          // Search isn't CRUD
POST /api/calculate       // Computation, not data

// Solutions:

// Option 1: Treat action result as a resource
POST /api/sessions        // Create a session (login)
DELETE /api/sessions/123  // Delete a session (logout)
POST /api/search-results  // Create a search result
POST /api/calculations    // Create a calculation

// Option 2: Use sub-resources for actions
POST /api/users/123/activate
POST /api/orders/456/cancel
POST /api/posts/789/publish

// Option 3: Accept that not everything is REST
// It's okay to have some RPC-style endpoints
POST /api/send-email
POST /api/generate-report
POST /api/process-payment

Key Takeaways

See Also