JSON and Server-Side APIs
Learning Objectives
- You know what JSON data is and can create and process JSON data with Hono.
- You know what application programming interfaces (APIs) are.
JSON data
In the previous chapters, we learned the very basics of working with requests in Deno and Hono. In the course, our primary use of server-side applications is to create application programming interfaces (APIs) that can be used to exchange data between a client and a server. In this chapter, we will look into how to create APIs that use JSON data for exchanging information.
JSON documents
A JSON document is a text document that contains data in a structured format. The data is represented as key-value pairs, where the keys are strings and the values can be strings, numbers, booleans, arrays, or other JSON objects. JSON documents are often used to exchange data between a server and a client, as they are easy to read and write for both humans and machines.
The following example outlines a simple JSON document that contains information about a book.
{
"id": 1,
"title": "How to break web applications",
"description": "A story of a teacher trying to create course materials.",
"chapters": [1, 2, 3, 4]
}
The above JSON document contains four key-value pairs, where the keys are id, title, description, and chapters. The values of the keys are a number, a string, another string, and an array of numbers, respectively.
JSON documents and JavaScript objects
The main difference between JSON and JavaScript objects is that JSON is a text format, while JavaScript objects are a data structure in the JavaScript programming language.
The keys in JSON documents must be strings, while in JavaScript objects, the keys can be any valid JavaScript identifier, including identifiers without quotes. Additionally, JSON documents must use double quotes for strings, while JavaScript objects can use single or double quotes.
The following example outlines how the above JSON document could be represented as a JavaScript object.
const book = {
id: 1,
title: "How to break web applications",
description: "A story of a teacher trying to create course materials.",
chapters: [1, 2, 3, 4],
};
JSON to JavaScript Object and Back
JavaScript contains built-in methods JSON.parse and JSON.stringify for converting between JSON documents and JavaScript objects. The following code snippet demonstrates how to convert a JSON document into a JavaScript object.
const json = '{"id":1,"title":"How to break web applications"}';
const book = JSON.parse(json);
console.log(book.title);
const jsonString = JSON.stringify(book);
console.log(jsonString);
The output of the above program is as follows:
How to break web applications
{"id":1,"title":"How to break web applications"}
We have already used these earlier in Persisting State with LocalStorage, as localstorage can only store string data. The methods JSON.parse and JSON.stringify were used to convert between JavaScript objects and JSON documents when storing and retrieving data from LocalStorage.
Hono and JSON data
Frameworks like Hono make it somewhat easier to work with JSON data, as they provide convenience methods for converting between JSON documents and JavaScript objects.
Responding with JSON data
Hono’s context object has a method json that can be used to transform a JavaScript object into a JSON document and to add it in the response. The method also sets the Content-Type header of the response to application/json, which indicates that the response body contains JSON data.
The Content-Type header indicates the media type of the resource. The media type
application/jsonindicates that the resource is in JSON format.
The following example outlines an application that responds with a JavaScript object with the property message that has the value Hello world!, transformed into JSON format.
import { Hono } from "@hono/hono";
const app = new Hono();
app.get("/", (c) => {
const data = { message: "Hello world!" };
return c.json(data);
},
);
export default app;
When we run the above program and query it, we see that the response is in JSON format.
$ curl localhost:8000
{"message":"Hello world!"}
Similarly, the following demonstrates how an application could respond in JSON format that has values extracted from path variables.
import { Hono } from "@hono/hono";
const app = new Hono();
app.get(
"/lists/:listId/items/:itemId",
(c) => {
const listId = c.req.param("listId");
const itemId = c.req.param("itemId");
return c.json({ listId, itemId });
},
);
export default app;
When we run the above program and query it, we see that the response is in JSON format.
$ curl localhost:8000/lists/1/items/3
{"listId":"1","itemId":"3"}
$ curl localhost:8000/lists/42/items/8
{"listId":"42","itemId":"8"}
The above example also shows a shorthand for creating JavaScript objects:
{ listId, itemId }. The shorthand can be used when creating objects where the property names and variable names are identical —{ listId, itemId }is equivalent to{ listId: listId, itemId: itemId }.
Reading JSON data from request
If a request has JSON data, the JSON data can be accessed using the asynchronous json method of the req property of the context. The following example outlines an application that responds with the JSON data that it receives.
import { Hono } from "@hono/hono";
const app = new Hono();
app.post("/", async (c) => {
const data = await c.req.json();
return c.json(data);
});
export default app;
When we run the above program and query it with a POST request, we see that the response is in JSON format.
$ curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello world!"}' localhost:8000
{"message":"Hello world!"}
Note that the method c.req.json() is asynchronous, as it may take some time to read the data from the request body. Due to this, it needs to be called with the await keyword to wait for the method to complete before proceeding. Furthermore, when a function contains an await expression, the function itself needs to be marked as asynchronous using the async keyword.
If the function would not be called with
await, it would return a Promise object instead of the actual data. A promise is a placeholder for a value that will be available in the future, once the asynchronous operation is complete.
The following example outlines an application that responds only with the property message of the JSON data that it receives if the property exists, otherwise responding with a message that the property is missing.
import { Hono } from "@hono/hono";
const app = new Hono();
app.post("/", async (c) => {
const data = await c.req.json();
const message = data.message ?? "Message missing";
return c.json({ message });
});
export default app;
When we run the above program and query it with a POST request, we see that the response is in JSON format.
$ curl -X POST -H "Content-Type: application/json" -d '{"message": "Test!"}' localhost:8000
{"message":"Test!"}
$ curl -X POST -H "Content-Type: application/json" -d '{}' localhost:8000
{"message":"Message missing"}
Server-side APIs
Application Programming Interfaces (APIs) refer to services that make it possible to communicate with systems that are hidden behind the API.
There are many different types of APIs, and there are no strict rules for what an API can or cannot do. APIs can be created for various purposes, including data retrieval, data storage, and data processing. An API could also, for example, be used to control a smart device, such as a door lock, a dishwasher, or a lamp.
As an example, Philips Hue smart lights can be controlled using an API, which — depending on the light bulb — allows e.g. changing the color, brightness, and on/off state of the light bulb.
When creating APIs, there are several aspects to consider, such as the protocol, the data representation format, and the security of the API. An API often has multiple endpoints for retrieving, updating, or deleting data.
In terms of the client-server model, most APIs are on the server-side, meaning that the server provides an API through which the client requests resources.
Here, we look into three small examples of what a server-side API could look like. The first one focuses on maintaining and updating a light bulb status, the second one on counting visits, and the third one on managing a list of books. In all of the examples, and in general in the server-side from now on, we prefix the API endpoints with /api to distinguish them from other endpoints that may be used for serving static files or other purposes. This is not a strict requirement, but it is a common practice to help organize the API endpoints.
Light bulb status
An API for changing the status of a light bulb could consist of two endpoints: one for retrieving the status and one for switching the status (on/off). HTTP GET method would be a natural choice for retrieving the status, while changing the status could be implemented with the HTTP POST.
The API could look as follows:
import { Hono } from "@hono/hono";
const app = new Hono();
let on = false;
app.get("/api/status", (c) => c.json({ on }));
app.post("/api/status", (c) => {
on = !on;
return c.json({ on });
});
export default app;
The above server would respond to requests as follows.
$ curl localhost:8000/api/status
{"on":false}
$ curl -X POST localhost:8000/api/status
{"on":true}
$ curl localhost:8000/api/status
{"on":true}
We could also have a third endpoint, which would be explicitly used to set the status of the light bulb to on or off. For this, the HTTP PUT method would be a suitable choice, as it is used to update a resource.
In this case, the API would also have to accept data in the request body, which would specify the new status of the light bulb. The following outlines how the API could be extended to include the PUT method.
import { Hono } from "@hono/hono";
const app = new Hono();
let on = false;
app.get("/api/status", (c) => c.json({ on }));
app.post("/api/status", (c) => {
on = !on;
return c.json({ on });
});
// new endpoint
app.put("/api/status", async (c) => {
const body = await c.req.json();
on = body.on;
return c.json({ on });
});
// new endpoint end
export default app;
Now, we could also explicitly set the status of the light bulb to on or off.
$ curl -X PUT -d '{"on":true}' localhost:8000/api/status
{"on":true}
$ curl localhost:8000/api/status
{"on":true}
$ curl -X PUT -d '{"on":false}' localhost:8000/api/status
{"on":false}
Visit counter
In a similar way, we could create an API for counting visits. The following outlines a resource-specific visit counter API that has three endpoints for retrieving, incrementing, and decrementing the count.
GET is used for retrieving the count, POST is used for incrementing the count, while DELETE is used for decrementing the count. The resource is specified as a path variable.
import { Hono } from "@hono/hono";
const app = new Hono();
const counts = new Map();
const getCount = (resource) => {
let count = 0;
if (counts.has(resource)) {
count = counts.get(resource);
}
return count;
}
app.get("/api/count/:resource", (c) => {
const resource = c.req.param("resource");
let count = getCount(resource);
return c.json({ count });
});
app.post("/api/count/:resource", (c) => {
const resource = c.req.param("resource");
let count = getCount(resource);
count++;
counts.set(resource, count);
return c.json({ count });
});
app.delete("/api/count/:resource", (c) => {
const resource = c.req.param("resource");
let count = getCount(resource);
count--;
counts.set(resource, count);
return c.json({ count });
});
export default app;
The resources could be e.g. site identifiers, page identifiers, or product identifiers. As an example, the API could be used to keep track of visits to a site identified with the string “creavite” as follows.
$ curl localhost:8000/api/count/creavite
{"count":0}
$ curl -X POST localhost:8000/api/count/creavite
{"count":1}
$ curl -X POST localhost:8000/api/count/creavite
{"count":2}
$ curl localhost:8000/api/count/creavite
{"count":2}
$ curl -X DELETE localhost:8000/api/count/creavite
{"count":1}
List of books
Another example could be an API for managing a list of books. Such an API could have three endpoints: GET for retrieving the list of books, POST for adding a book to the list, and DELETE for removing a book from the list.
For simplicity, we assume that the books have an id property, which is unique for each book. When deleting, we only need to provide information about the id of the book to be removed.
The following outlines how the API could be implemented.
import { Hono } from "@hono/hono";
const app = new Hono();
let books = [];
app.get("/api/books", (c) => c.json({ books }));
app.post("/api/books", async (c) => {
const body = await c.req.json();
books.push(body);
return c.json({ books });
});
app.delete("/api/books", async (c) => {
const body = await c.req.json();
books = books.filter((book) => book.id !== body.id);
return c.json({ books });
});
export default app;
The above server would respond to requests as follows.
$ curl localhost:8000/api/books
{"books":[]}
$ curl -X POST -d '{"id":1,"title":"Curling books"}' localhost:8000/api/books
{"books":[{"id":1,"title":"Curling books"}]}
$ curl -X POST -d '{"id":2,"title":"Books API"}' localhost:8000/api/books
{"books":[{"id":1,"title":"Curling books"},{"id":2,"title":"Books API"}]}
$ curl -X DELETE -d '{"id":1}' localhost:8000/api/books
{"books":[{"id":2,"title":"Books API"}]}
Summary
In summary:
- JSON is a text-based format for structured data, commonly used for exchanging information between clients and servers.
- Hono simplifies working with JSON through its context methods:
c.json()for sending JSON responses andc.req.json()for reading JSON request bodies. - APIs (Application Programming Interfaces) provide access to resources or functionality, typically through server-side endpoints.
- Common API patterns include using GET for retrieval, POST for creation or updates, PUT for replacing resources, and DELETE for removal.
- API endpoints are often prefixed with
/apito separate them from other routes.