Read
Learning objectives
- You know (and rehearse) how to implement functionality for reading resources.
Now we have the functionality to create a todo. Let's next add the functionality for reading todos.
Listing todos
To list todos, we need the functionality for reading the todos from the database and rendering them into a view template. Let's start by creating the service functionality for reading todos, after which we add the functionality for rendering todos.
Reading a list from Deno KV
To read a list of todos from the database, we can use Deno KV's list
method. The method is given an object as a parameter, which contains an attribute prefix
. The attribute prefix
is given a list of keys that indicate the key (or key combination) with which to search for entries. In our case, we look for entries associated with the key "todos".
The method returns an asynchronous list iterator with objects with the keys and values of the items that match the prefix. Retrieving all the todos and adding them into a list works as follows.
const listTodos = async () => {
const kv = await Deno.openKv();
const entries = await kv.list({ prefix: ["todos"] });
const todos = [];
for await (const entry of entries) {
todos.push(entry.value);
}
return todos;
};
With the above added to the todoService.js
, our todoService.js
looks now as follows.
const createTodo = async (todo) => {
todo.id = crypto.randomUUID();
const kv = await Deno.openKv();
await kv.set(["todos", todo.id], todo);
};
const listTodos = async () => {
const kv = await Deno.openKv();
const todoEntries = await kv.list({ prefix: ["todos"] });
const todos = [];
for await (const entry of todoEntries) {
todos.push(entry.value);
}
return todos;
};
export { createTodo, listTodos };
Linking the service to the controller
Next, we need to link the service to the controller. In todoController.js
, we already import the functionality exported by todoService.js
, so we need to essentially find a place where we plug in the listTodos
functionality.
One meaningful location would be the page with the form for creating a todo, which is handled by the showForm
function of todoController.js
. Let's use that -- the key change is passing data to the view template in the showForm
function. After the change, the function looks as follows.
const showForm = async (c) => {
return c.html(
eta.render("todos.eta", { todos: await todoService.listTodos() }),
);
};
Listing the todos in the view template
With the above change, the view template will have access to existing todos through the it.todos
variable. Let's add the functionality for listing the todos in the view template.
We've already practiced rendering lists and objects when learning about view templates.
To list the todos, let's add an unordered list element to the todos.eta
, and iterate over the todos with the it.todos
variable. For each todo, we add a list item element with the todo's name -- which is in the todo
attribute of the todo object.
It might have been meaningful to use some other variable name like
title
to indicate the name of the todo.. Oh well..
In the Eta view template, this would look as follows.
<ul>
<% it.todos.forEach((todo) => { %>
<li><%= todo.todo %></li>
<% }); %>
</ul>
With the above added to the todos.eta
, and with a few new paragraphs indicating the contents, the todos.eta
looks as follows.
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Add a todo:</p>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
<p>Existing todos:</p>
<ul>
<% it.todos.forEach((todo) => { %>
<li><%= todo.todo %></li>
<% }); %>
</ul>
</body>
</html>
Now, when we make a GET request to the path /todos
on the server, we are shown the form and the todos from the database.
curl localhost:8000/todos
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Add a todo:</p>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
<p>Existing todos:</p>
<ul>
<li>Rest</li>
</ul>
</body>
</html>
Similarly, when we make a POST request to add a new entry, we receive a redirect response, and on a subsequent GET request, we see that the new todo has been added.
curl -v -X POST -d "todo=Eat" localhost:8000/todos
...
< HTTP/1.1 302 Found
< location: /todos
...
%
curl localhost:8000/todos
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Add a todo:</p>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
<p>Existing todos:</p>
<ul>
<li>Rest</li>
<li>Eat</li>
</ul>
</body>
</html>
Reading a single todo
Read functionality in CRUD is often associated also with reading a single entry. Let's add the functionality for reading and showing a single todo.
To show a single todo, we need to add individual links to the page for the individual todos, a new route to the router that corresponds to the links, a new function to the controller, a new function to the service, and a new view template. Let's start by adding the links.
Adding links to the view template
To add links, we use the identifier that we added to the todo items when creating them. Because of this, each todo comes a unique id
attribute, which we can use to link to a page with information on the specific todo.
To create the links, we modify the view template todos.eta
so that each list item contains a link to a todo-specific page. We can use the todo.id
attribute to create the link -- let's assume that the path for individual todos will be identified using a path variable and that individual todo's will be mapped to /todos/:id
.
The modified view template looks as follows.
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Add a todo:</p>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
<p>Existing todos:</p>
<ul>
<% it.todos.forEach((todo) => { %>
<li><a href="/todos/<%= todo.id %>"><%= todo.todo %></a></li>
<% }); %>
</ul>
</body>
</html>
Now, when we make a request to the server, we see that the links are added to the page.
curl localhost:8000/todos
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Add a todo:</p>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
<p>Existing todos:</p>
<ul>
<li><a href="/todos/9ab14718-439c-4621-829d-f4f7b0737562">Rest</a></li>
<li><a href="/todos/de446c5f-15d5-4e89-8bf3-e7beb6682a02">Eat</a></li>
</ul>
</body>
</html>
When we try to access one of the pages, we see a response indicating that the resource was not found.
curl localhost:8000/todos/9ab14718-439c-4621-829d-f4f7b0737562
404 Not Found%
This is to be expected -- we haven't yet added the route to the router, nor the functionality to the controller, nor the functionality to the service. Let's add them next.
Routing requests
To route requests to the individual todo pages, we need to add a new route to the router. Let's add the route to the app.js
file. The route should be a GET request to the path /todos/:id
, and it should be handled by the showTodo
function of the todoController.js
.
After the modification, the app.js
looks as follows.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
import * as todoController from "./todoController.js";
const app = new Hono();
app.get("/todos", todoController.showForm);
app.get("/todos/:id", todoController.showTodo);
app.post("/todos", todoController.createTodo);
Deno.serve(app.fetch);
At the moment, the showTodo
function is still missing from todoController.js
. Thus, making a query to the server returns an internal server error.
curl localhost:8000/todos/9ab14718-439c-4621-829d-f4f7b0737562
Internal Server Error%
Responding from the controller
Let's add first basic functionality to the controller that just responds with a text that tells what the id of the of the todo that was asked for is.
Let's create the showTodo
function to todoController.js
. The showTodo
function needs to read the path variable id
from the request, and respond with the id in text format. To read the path variable, we can use the c.req.param
method, which takes the path variable identifier -- in our case id
-- as a parameter.
We learned about path variables when learning about the basics of the Hono Web Framework.
The showTodo
function looks as follows.
const showTodo = async (c) => {
const id = c.req.param("id");
return c.text("Asking for a todo with id " + id);
};
In addition to adding the function, we also need to export it. As a whole, the todoController.js
looks currently as follows.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
import * as todoService from "./todoService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const showForm = async (c) => {
return c.html(
eta.render("todos.eta", { todos: await todoService.listTodos() }),
);
};
const createTodo = async (c) => {
const body = await c.req.parseBody();
await todoService.createTodo(body);
return c.redirect("/todos");
};
const showTodo = async (c) => {
const id = c.req.param("id");
return c.text("Asking for todo " + id);
};
export { createTodo, showForm, showTodo };
Now, a request to the server returns the id of the todo that was asked for.
curl localhost:8000/todos/9ab14718-439c-4621-829d-f4f7b0737562
Asking for todo 9ab14718-439c-4621-829d-f4f7b0737562%
Retrieving the todo from the service
To retrieve the todo from the service, we need to add the functionality to the service. Let's add the functionality to the todoService.js
. Let's call the function getTodo
. The function should take the id of the todo as a parameter, and it should return the todo with the given id.
To retrieve the todo, we can use the get
method of Deno KV. The method takes a list of keys as a parameter -- in our case, the list includes "todos" and the identifier that we are using to search for the specific todo. The method returns an entry -- the attribute value
of the entry contains the concrete todo object.
The getTodo
function looks as follows -- the function returns an empty object if the todo is not found.
const getTodo = async (id) => {
const kv = await Deno.openKv();
const todo = await kv.get(["todos", id]);
return todo?.value ?? {};
};
After adding the getTodo
to the exports of todoService.js
, the full todoService.js
looks now as follows.
const createTodo = async (todo) => {
todo.id = crypto.randomUUID();
const kv = await Deno.openKv();
await kv.set(["todos", todo.id], todo);
};
const listTodos = async () => {
const kv = await Deno.openKv();
const todoEntries = await kv.list({ prefix: ["todos"] });
const todos = [];
for await (const entry of todoEntries) {
todos.push(entry.value);
}
return todos;
};
const getTodo = async (id) => {
const kv = await Deno.openKv();
const todo = await kv.get(["todos", id]);
return todo?.value ?? {};
};
export { createTodo, getTodo, listTodos };
To test whether the function works, let's adjust the todoController.js
so that it uses the getTodo
function to retrieve the todo from the database and responds with the found todo in text format. The showTodo
function looks as follows -- the JSON.stringify
function transforms an object into a string.
const showTodo = async (c) => {
const id = c.req.param("id");
const todo = await todoService.getTodo(id);
return c.text("Found todo: " + JSON.stringify(todo));
};
Now, when we make a request to the server, we see the todo that was found from the database.
curl localhost:8000/todos/9ab14718-439c-4621-829d-f4f7b0737562
Found todo: {"todo":"Rest","id":"9ab14718-439c-4621-829d-f4f7b0737562"}
Adding a view template and rendering it
To show the todo in a more readable format, let's create a view template for the todo. Let's call the view template todo.eta
, and place it to the templates
folder. For now, the template can simply contain the name of the todo and a link back to the list of todos -- the contents of the file would be as follows.
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Todo:</p>
<p><%= it.todo.todo %></p>
<p><a href="/todos">Back to todos</a></p>
</body>
</html>
Now that the view template is in place, we can render it in the showTodo
function of the todoController.js
. After adjustments, the todoController.js
looks as follows.
const showTodo = async (c) => {
const id = c.req.param("id");
return c.html(
eta.render("todo.eta", { todo: await todoService.getTodo(id) }),
);
};
Now, when we make a request to the server, we see the todo as a part of the rendered html.
curl localhost:8000/todos/9ab14718-439c-4621-829d-f4f7b0737562
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<p>Todo:</p>
<p>Rest</p>
<p><a href="/todos">Back to todos</a></p>
</body>
</html>
The next functionality in line is the update functionality. Let's get to it next.