RESTful APIs
Learning objectives
- You know of RESTful APIs.
One of the most common ways of implementing APIs in web applications is following REST, originally discussed in the thesis Architectural Styles and the Design of Network-based Software Architectures. The thesis broadly discusses different architectural styles and how there is no silver bullet, while the chapter on REST (Representational State Transfer) outlines how to sensibly build web applications. REST is not a standard, but rather a set of constraints or guidelines.
Although the chapter on REST in the dissertation does not explicitly state that HTTP should be used (as the idea is protocol-agnostic), HTTP is currently the most common way for communication.
Features of RESTful APIs
Presently, RESTful APIs (aka. REST APIs) are mostly understood as having the following features:
HTTP-based client-server architecture: APIs use client-server architecture, where the client and the server are separated from each other and requests are made over HTTP.
Stateless communication: APIs are stateless, which means that the server does not need to store any information about the session. Each request is independent of the previous request.
Identifiable resources: APIs are about resources (e.g. documents, images, temporal services, ...) that have identifiers, i.e. they can be identified by some means. In the context of RESTful APIs, the identifiers are URIs.
Representable resources: The resources are represented in a format that is suitable for the client. Currently, the most common format is JSON.
Manipulation of resources through representations: The resources can be manipulated through representations and their identifiers, i.e. a client can send a representation of the resource (e.g., a JSON document) to an address that identifies a resource (e.g., an URI), which on the server can lead to the manipulation (e.g. update) of the resource.
Self-descriptive messages: The messages that are sent between the client and the server are self-descriptive, i.e. they contain the information that is needed to understand the message. In the context of HTTP, this includes using HTTP status codes.
In practice, over time, the way how RESTful APIs are implemented has changed as has the interpretation of the term RESTful APIs. Let's next look at how a RESTful API, as understood today, could look like.
API design and REST
Contemporary RESTful APIs use HTTP as the communication protocol, which is stateless by design. Resources are identified using URIs, and the HTTP methods (GET, POST, PUT, DELETE) are used to manipulate resources. Depending on the manipulation operation, the request may contain a resource (e.g. when adding or updating a resource), while the response may contain a resource (e.g. when retrieving a resource). The HTTP status codes are used to indicate the status of the request, providing self-descriptive messages.
As a concrete example, a RESTful API for handling addresses could be as follows.
GET /addresses
: Retrieve all addresses and return the addresses in JSON format.POST /addresses
: Using JSON data from the request body, create a new address.GET /addresses/:id
: Retrieve an address with the given id and return the address in JSON format.PUT /addresses/:id
: Using JSON data from the request body, update an address with the given id.DELETE /addresses/:id
: Delete an address with the given id.
With Hono, the above could be implemented as follows. Here, we assume that we have the following addresses
table that was added to the Docker project in the part Database Migrations of the chapter Application Containerization.
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
address TEXT NOT NULL
);
We also assume that we have the database.js
file in the same directory as the application, and that the database.js
exports the sql
function that can be used to create database queries. Furthermore, we assume that Hono is exported from deps.js
.
import { Hono } from "./deps.js";
import { sql } from "./database.js";
const app = new Hono();
app.get("/addresses", async (c) => {
return c.json(await sql`SELECT * FROM addresses`);
});
app.post("/addresses", async (c) => {
const body = await c.req.json();
await sql`INSERT INTO addresses (name, address) VALUES (${body.name}, ${body.address})`;
return c.json({ status: "ok" });
});
app.get("/addresses/:id", async (c) => {
const id = c.req.param("id");
const rows = await sql`SELECT * FROM addresses WHERE id = ${id}`;
if (rows.length === 0) {
return c.json({ status: "Not found" }, 404);
}
return c.json(rows[0]);
});
app.put("/addresses/:id", async (c) => {
const id = c.req.param("id");
const body = await c.req.json();
await sql`UPDATE addresses SET name = ${body.name}, address = ${body.address} WHERE id = ${id}`;
// here we assume that such an address exists -- could also check whether one exists
return c.json({ status: "ok" });
});
app.delete("/addresses/:id", async (c) => {
const id = c.req.param("id");
await sql`DELETE FROM addresses WHERE id = ${id}`;
// here we assume that such an address exists -- could also check whether one exists
return c.json({ status: "ok" });
});
Deno.serve(app.fetch);
The above provides a straightforward way of implementing a RESTful API that handles a single resource. In practice, RESTful APIs can be much more complex and they can relate to more complex data. In the next part, we look into an existing API, and learn a bit about learning to comprehend and use existing APIs.