Web APIs and REST

REST, which stands for Representational State Transfer, is an architectural style for designing networked applications. It was introduced by Roy Fielding in his doctoral dissertation in 2000 as a way to guide the design of scalable and simple web systems. REST is not a protocol or a standard but rather a set of principles that leverage the foundational elements of the web.

The web was originally envisioned as a system for sharing and linking documents. REST builds on this vision by emphasizing simplicity, scalability, and statelessness. Fielding's work aimed to formalize the principles that made the web successful, such as the use of standard HTTP methods and the concept of resources identified by URLs. RESTful systems are designed to work seamlessly with the web's existing infrastructure, making them lightweight and easy to implement.

At its core, REST revolves around the idea of resources. A resource can be anything that can be named, such as a user, a blog post, or a product. Each resource is identified by a unique URL, which acts as its address on the web.

RESTful APIs use HTTP methods as verbs to perform actions on these resources. The most common HTTP methods include:

  • GET: Retrieve a representation of a resource.
  • POST: Create a new resource.
  • PUT: Update an existing resource.
  • DELETE: Remove a resource.

In addition to verbs and nouns, REST also incorporates "adjectives" in the form of query parameters. Query parameters allow clients to refine their requests, such as filtering, sorting, or paginating data. For example, a URL like /products?category=electronics&sort=price retrieves a list of products in the electronics category, sorted by price.

REST aligns closely with the original intent of the web as a decentralized and stateless system. By using standard HTTP methods and URLs, RESTful APIs are inherently interoperable and accessible. They do not require complex protocols or additional layers of abstraction, making them easier to understand and use.

REST vs. Web Services

Before REST gained popularity, web services often relied on protocols like SOAP (Simple Object Access Protocol). SOAP (Simple Object Access Protocol) was introduced in 1999 as a protocol for exchanging structured information in the implementation of web services. It is based on XML and relies on a set of standards for message formatting, security, and error handling. While SOAP was a significant step forward in enabling machine-to-machine communication, it came with a steep learning curve and a high level of complexity.

One of the challenges with SOAP was its reliance on XML, which is verbose and difficult to parse compared to modern formats like JSON. Additionally, SOAP required developers to work with WSDL (Web Services Description Language) files to define the structure of the service, as well as complex specifications for security (WS-Security) and transactions (WS-AtomicTransaction). These additional layers made SOAP-based systems heavyweight and harder to implement and maintain.

SOAP also required strict adherence to its protocol, which often led to interoperability issues between different systems. Developers needed specialized tools and libraries to work with SOAP, further increasing the barrier to entry.

By the mid-2000s, as RESTful APIs began to gain traction, SOAP started to decline in popularity. REST's simplicity, combined with its alignment with the web's architecture, made it a more attractive option for developers. The rise of AJAX and JSON in the late 2000s further accelerated this shift, as RESTful APIs were better suited for the lightweight, asynchronous communication required by modern web applications.

By the early 2010s, REST had largely supplanted SOAP as the preferred approach for building web APIs. While SOAP is still used in certain enterprise environments where its advanced features are necessary, its complexity has made it less appealing for most web-based applications.

REST and JSON: A Perfect Match

Although REST can work with various data formats, including HTML and XML, it became particularly popular with the rise of JSON (JavaScript Object Notation). JSON is lightweight, easy to read, and natively supported by JavaScript, making it an ideal format for APIs consumed by AJAX-based applications. This combination of REST and JSON has become the backbone of modern web development, enabling seamless communication between clients and servers.

Pro Tipđź’ˇ In recent years, there has been a growing trend away from JSON-based REST APIs and back toward using HTML as the primary representation of state. This approach aligns with the concept of HATEOAS (Hypermedia as the Engine of Application State), a key principle of REST that emphasizes the use of hypermedia to drive application behavior. Frameworks like HTMX have popularized this shift by enabling developers to build dynamic, interactive web applications without relying heavily on JavaScript or JSON APIs. HTMX allows clients to make HTTP requests directly from HTML elements, seamlessly updating parts of the page with server-rendered HTML. This approach simplifies development by leveraging the server's ability to generate and manage stateful HTML, reducing the need for complex client-side logic. By returning to HTML as the primary medium for state representation, developers can create applications that are more accessible, easier to debug, and better aligned with the original principles of the web. This trend highlights the enduring relevance of REST's foundational ideas while adapting them to modern development practices.

Managing Customers with a REST API in Express

To demonstrate how RESTful APIs work in practice, let's create a simple Express-based API to manage a list of customers. This API will support the following operations:

  1. List all customers
  2. Get a specific customer by ID
  3. Create a new customer
  4. Update an existing customer
  5. Delete a customer

Example Endpoints

Typically URLs that are associated with a web API are called endpoints. They are specific URLs that can be interacted with to perform data operations. Just like some function calls require parameters when called, some endoints will need request parameters - which are commonly JSON data sent as part of the request body. These are sometimes called payloads.

1. List All Customers

Endpoint: GET /customers
Response:

[
    {
        "id": 1,
        "name": "John Doe",
        "email": "john.doe@example.com"
    },
    {
        "id": 2,
        "name": "Jane Smith",
        "email": "jane.smith@example.com"
    }
]

2. Get a Specific Customer by ID

Endpoint: GET /customers/:id
Example Request: GET /customers/1
Response:

{
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@example.com"
}

3. Create a New Customer

Endpoint: POST /customers
Request Payload:

{
    "name": "Alice Johnson",
    "email": "alice.johnson@example.com"
}

Response:

{
    "id": 3,
    "name": "Alice Johnson",
    "email": "alice.johnson@example.com"
}

4. Update an Existing Customer

Endpoint: PUT /customers/:id
Example Request: PUT /customers/1
Request Payload:

{
    "name": "Johnathan Doe",
    "email": "johnathan.doe@example.com"
}

Response:

{
    "id": 1,
    "name": "Johnathan Doe",
    "email": "johnathan.doe@example.com"
}

5. Delete a Customer

Endpoint: DELETE /customers/:id
Example Request: DELETE /customers/1
Response:

{
    "message": "Customer with ID 1 has been deleted."
}

Example Express Code

Below is an example implementation of these endpoints using Express:

const express = require('express');
const app = express();
app.use(express.json());

let customers = [
    { id: 1, name: 'John Doe', email: 'john.doe@example.com' },
    { id: 2, name: 'Jane Smith', email: 'jane.smith@example.com' }
];

// List all customers
app.get('/customers', (req, res) => {
    res.json(customers);
});

// Get a specific customer by ID
app.get('/customers/:id', (req, res) => {
    const customer = customers.find(c => c.id === parseInt(req.params.id));
    if (!customer) return res.status(404).json({ error: 'Customer not found' });
    res.json(customer);
});

// Create a new customer
app.post('/customers', (req, res) => {
    const newCustomer = {
        id: customers.length + 1,
        name: req.body.name,
        email: req.body.email
    };
    customers.push(newCustomer);
    res.status(201).json(newCustomer);
});

// Update an existing customer
app.put('/customers/:id', (req, res) => {
    const customer = customers.find(c => c.id === parseInt(req.params.id));
    if (!customer) return res.status(404).json({ error: 'Customer not found' });

    customer.name = req.body.name || customer.name;
    customer.email = req.body.email || customer.email;
    res.json(customer);
});

// Delete a customer
app.delete('/customers/:id', (req, res) => {
    const customerIndex = customers.findIndex(c => c.id === parseInt(req.params.id));
    if (customerIndex === -1) return res.status(404).json({ error: 'Customer not found' });

    customers.splice(customerIndex, 1);
    res.json({ message: `Customer with ID ${req.params.id} has been deleted.` });
});

// Start the server
const PORT = 3000;
app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});

This example demonstrates how RESTful principles can be applied to manage resources in a web application. Each endpoint corresponds to a specific HTTP method and URL, making the API intuitive and easy to use.

Pro Tipđź’ˇ Where's the HTML? - You might be wondering, why is there no HTML at all in the example above? You are only seeing one part of a hypothetical web application that uses AJAX and REST. Somewhere else (not shown in the example) is an HTML document - much like the one we wrote in the last chapter when creating our client-side only Guessing Game. That HTML has JavaScript that will call these endpoints, and build/modify the HTML DOM the user is seeing. Be patient, in the next section we'll tie this all together, and use an API combined with HTML/Client-side JavaScript.

Understanding :param Notation in Express

In Express, the :param notation is used to define route parameters. These parameters act as placeholders in the URL and allow you to capture dynamic values from the request URL. For example, in the route GET /customers/:id, the :id part is a route parameter that can be accessed in the request handler using req.params.id.

Example of Using :param

app.get('/customers/:id', (req, res) => {
    const customerId = req.params.id;
    res.send(`Customer ID is: ${customerId}`);
});

If a client sends a request to /customers/42, the req.params.id will contain the value 42.

Data Type Validation for Route Parameters

By default, route parameters are treated as strings. If you need to validate or enforce specific data types (e.g., ensuring :id is a number), you can use middleware or validation libraries like Joi or express-validator.

Example of Manual Validation

app.get('/customers/:id', (req, res) => {
    const customerId = parseInt(req.params.id, 10);
    if (isNaN(customerId)) {
        return res.status(400).json({ error: 'Invalid customer ID. It must be a number.' });
    }
    res.send(`Customer ID is: ${customerId}`);
});

Example Using express-validator

const { param, validationResult } = require('express-validator');

app.get('/customers/:id', [
    param('id').isInt().withMessage('Customer ID must be an integer')
], (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }
    res.send(`Customer ID is: ${req.params.id}`);
});

Importance of Route Order

In Express, routes are evaluated in the order they are defined. If a more generic route (e.g., GET /customers/:id) is defined before a more specific route (e.g., GET /customers/all), the generic route will match first, potentially causing unexpected behavior.

Example of Conflicting Routes

// Generic route
app.get('/customers/:id', (req, res) => {
    res.send(`Customer ID: ${req.params.id}`);
});

// Specific route
app.get('/customers/all', (req, res) => {
    res.send('List of all customers');
});

In this case, a request to /customers/all would incorrectly match the GET /customers/:id route, treating all as the :id parameter.

Correct Route Order

To avoid conflicts, always define more specific routes before generic ones:

// Specific route
app.get('/customers/all', (req, res) => {
    res.send('List of all customers');
});

// Generic route
app.get('/customers/:id', (req, res) => {
    res.send(`Customer ID: ${req.params.id}`);
});

By carefully ordering your routes and validating route parameters, you can ensure your Express application behaves predictably and handles requests robustly.

Query Strings and HTTP Verbs in REST APIs

Query Strings in REST APIs

Query strings are a mechanism for appending additional parameters to a URL in order to refine or customize a request. They are typically used in conjunction with the GET method to filter, sort, or paginate data. Query strings follow the ? character in a URL and consist of key-value pairs separated by &.

Example of Query Strings

  • Filtering: /products?category=electronics
  • Sorting: /products?sort=price
  • Pagination: /products?page=2&limit=10
  • Combined: /products?category=electronics&sort=price&page=2

Query strings allow clients to specify exactly what data they need without altering the structure of the API. This makes them a powerful tool for creating flexible and efficient endpoints.

GET: A Read-Only Operation

The GET method is used to retrieve data from a server. It is considered a read-only operation, meaning it should never modify the state of the server or its resources. This principle ensures that GET requests are idempotent and safe, allowing them to be cached, bookmarked, or repeated without unintended side effects.

Example of a GET Request

  • URL: GET /customers/1
  • Response: Returns the details of the customer with ID 1.

Since GET does not change the server's state, it is ideal for operations like fetching data, searching, or displaying information.

HTTP Verbs in REST APIs

REST APIs use HTTP verbs to define the type of operation being performed on a resource. Each verb has a specific purpose and semantic meaning:

1. GET

  • Purpose: Retrieve a representation of a resource.
  • Characteristics: Read-only, idempotent, and safe.
  • Example: GET /customers retrieves a list of customers.

2. POST

  • Purpose: Create a new resource on the server.
  • Characteristics: Not idempotent (repeating the request creates multiple resources).
  • Example: POST /customers with a payload creates a new customer.

3. PUT

  • Purpose: Update an existing resource or create it if it does not exist (idempotent behavior).
  • Characteristics: Idempotent (repeating the request has the same effect as a single request).
  • Example: PUT /customers/1 updates the customer with ID 1.

4. PATCH

  • Purpose: Partially update an existing resource.
  • Characteristics: Not necessarily idempotent, depending on implementation.
  • Example: PATCH /customers/1 updates specific fields of the customer with ID 1.

5. DELETE

  • Purpose: Remove a resource from the server.
  • Characteristics: Idempotent (repeating the request has the same effect as a single request).
  • Example: DELETE /customers/1 deletes the customer with ID 1.

Summary of HTTP Verbs

VerbPurposeIdempotentSafe
GETRetrieve dataYesYes
POSTCreate a resourceNoNo
PUTUpdate or createYesNo
PATCHPartially updateNoNo
DELETERemove a resourceYesNo

By adhering to these conventions, REST APIs remain predictable, intuitive, and aligned with the principles of the web.

Authentication and Authorization

Authentication and authorization are critical components of any API, ensuring that only authorized users or systems can access protected resources. Traditionally, web applications have relied on sessions and interactive logins to manage user authentication. This approach, which involves storing session data on the server and using cookies to maintain state, is still a perfectly valid and widely used method. It works well for browser-based applications where users interact directly with the interface.

However, in scenarios where REST APIs are consumed by other applications, scripts, or services—rather than by users through a browser—interactive logins are not practical. For example, a mobile app or a backend service calling an API cannot easily handle a login form or manage cookies. This is where API tokens come into play.

API Tokens: A Simple Solution

API tokens are unique identifiers, often in the form of GUIDs (Globally Unique Identifiers), that act as a key to access the API. When a client authenticates successfully, the server generates a token and provides it to the client. The client then includes this token in the headers of subsequent API requests, allowing the server to identify and authorize the client.

Here’s an example of how to generate and use a simple API token in a Node.js application:

Generating an API Token

const crypto = require('crypto');

// Function to generate a simple API token
function generateToken() {
    return crypto.randomUUID(); // Generates a unique GUID
}

// Example usage
const token = generateToken();
console.log(`Generated API Token: ${token}`);

Using the Token in an API

const express = require('express');
const app = express();

const validTokens = new Set(); // Store valid tokens (in-memory for simplicity)

// Middleware to authenticate requests using the token
app.use((req, res, next) => {
    const token = req.headers['authorization'];
    if (!token || !validTokens.has(token)) {
        return res.status(401).json({ error: 'Unauthorized' });
    }
    next();
});

// Endpoint to issue a new token
app.post('/auth/token', (req, res) => {
    const newToken = generateToken();
    validTokens.add(newToken);
    res.json({ token: newToken });
});

// Protected endpoint
app.get('/protected', (req, res) => {
    res.json({ message: 'You have accessed a protected resource!' });
});

// Start the server
const PORT = 3000;
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

In this example, the /auth/token endpoint issues a new token, which the client must include in the Authorization header of subsequent requests. This approach is simple and effective for many use cases.

The Tip of the Iceberg: Tokens, JWTs, and Beyond

While simple tokens like GUIDs are a good starting point, the world of API authentication is vast and complex. Tokens can take many forms, including:

  • Access Tokens: Short-lived tokens used to access specific resources.
  • Refresh Tokens: Longer-lived tokens used to obtain new access tokens without requiring the user to reauthenticate.
  • JWT (JSON Web Tokens): Self-contained tokens that include encoded information about the user or client, often signed to ensure integrity.

JWTs are particularly popular because they allow for stateless authentication. A JWT contains all the information needed to verify the client, eliminating the need for server-side session storage. However, implementing JWTs securely can be challenging. For example, if a JWT is compromised, it cannot be revoked without additional mechanisms. As a result, many "stateless" JWT implementations end up reintroducing session-like behavior, such as maintaining a token blacklist or using refresh tokens.

Stateless Authentication and HTTP's Stateless Nature

One of the key advantages of tokens, especially JWTs, is their alignment with the stateless nature of HTTP. In a stateless system, each request is independent and contains all the information needed for authentication. This eliminates the need for the server to maintain session state, making the system more scalable and resilient.

HTTP itself was designed to be stateless, and mechanisms like HTTP Basic Authentication and Bearer Tokens reflect this principle. In these approaches, the client includes authentication credentials (e.g., a username and password or a token) with every request. While this can simplify server-side implementation, it also places a greater burden on the client to manage and protect credentials.

Balancing Simplicity and Security

Ultimately, the choice of authentication method depends on the specific needs of your application. For simple use cases, session-based authentication or basic API tokens may suffice. For more complex scenarios, such as distributed systems or third-party integrations, advanced token-based mechanisms like JWTs or OAuth2 may be necessary.

However, it’s important to remember that no solution is one-size-fits-all. Stateless authentication offers scalability and simplicity, but it requires careful design to ensure security. Conversely, session-based authentication provides robust control but may introduce challenges in distributed environments.

By understanding the trade-offs and principles behind each approach, you can design an authentication system that meets the needs of your application while adhering to best practices.

REST APIs and Mobile App Development

Mobile applications have become an integral part of modern life, and REST APIs play a crucial role in their functionality. At their core, mobile apps are often just specialized clients that interact with centralized servers, much like web browsers. These apps frequently use the same REST API endpoints as their web-based counterparts, enabling seamless integration and consistency across platforms.

Mobile Apps as Specialized Browsers

A mobile app can be thought of as a tailored interface for accessing web-based resources. Instead of rendering HTML and CSS like a browser, mobile apps use native components to display data retrieved from REST APIs. For example, a shopping app might fetch product details from the same /products endpoint used by the website, but display the information using native UI elements instead of a web page.

This shared use of REST APIs simplifies development by allowing a single backend to serve multiple clients, including web browsers, mobile apps, and even other services. It also ensures that data and business logic remain consistent across all platforms.

Why REST APIs Are Ideal for Mobile Apps

REST APIs have become the de facto standard for building applications, including mobile apps, due to several key advantages:

  1. Ubiquity of HTTP: HTTP is the most widely used protocol for communication over networks. It is supported by virtually all devices and easily traverses firewalls, making it an ideal choice for mobile apps that need to communicate with centralized servers.

  2. Statelessness: REST's stateless nature aligns well with the architecture of mobile apps. Each API request contains all the information needed to process it, reducing the need for persistent connections and enabling scalability.

  3. Centralized Servers and Databases: Mobile apps almost always rely on centralized servers to store and manage data. REST APIs provide a standardized way for apps to interact with these servers, whether they are fetching user profiles, submitting orders, or syncing data.

  4. Cross-Platform Compatibility: By using REST APIs, developers can create a single backend that serves multiple platforms, including iOS, Android, and web. This reduces duplication of effort and ensures a consistent user experience.

The Necessity of Web Servers for Mobile Apps

In many ways, a mobile app cannot exist without a web server. The server acts as the backbone of the application, handling tasks such as:

  • Data Storage: Centralized databases store user data, application settings, and other critical information.
  • Authentication: Servers manage user authentication and authorization, ensuring secure access to resources.
  • Business Logic: Complex operations, such as processing payments or generating reports, are often handled on the server side.
  • Real-Time Updates: Servers enable features like push notifications and live data synchronization, enhancing the user experience.

Without a web server and its accompanying REST API, a mobile app would be limited to offline functionality, severely restricting its capabilities.

REST APIs: The Backbone of Modern Applications

The widespread adoption of REST APIs has transformed how applications are built, making them the backbone of modern development. Whether it's a mobile app, a web application, or an IoT device, REST APIs provide a universal way to communicate over networks. Their simplicity, scalability, and alignment with HTTP have made them indispensable for developers, particularly in the context of mobile app development.

By leveraging REST APIs, developers can create powerful, interconnected systems that deliver a seamless experience across devices, ensuring that mobile apps remain a cornerstone of the digital ecosystem.

The HTML version of this book is free. If you find it useful, please consider supporting it by buying the PDF, E-PUB, or paperback. If you are one of my students, please don't do this - just email me ;)

Kindle $9.99

E-book + PDF $14.99