Create
Learning objectives
- You know (and rehearse) how to implement functionality for creating resources.
To create a todo item, we need a form, a route, the controller functionality, and the service functionality. Let's start by creating an application that responds with a form that can be used to create todos, after which we create the functionality for storing the submitted form data into the database.
Showing a form for adding todos
To show a form for adding todos, we need to have the form and a route that responds with the form.
Form for adding todos
The form for adding todos will be a simple form with a single input field and a submit button. The form will have a label "Todo: " for the input field, and the input field will be identified (and named) todo. The form will be submitted to path /todos
using POST. In HTML, this looks as follows.
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
</body>
</html>
The above form is saved to the file todos.eta
in the templates
folder of the project.
Showing the form on requests
To show the form on requests, let's create the core application functionality to app.js
. The file imports Hono and Eta, configures Eta, and creates a route that responds with the a rendered todos.eta
that contains the form. We'll use GET /todos
as the page that shows the form.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const app = new Hono();
app.get("/todos", (c) => c.html(eta.render("todos.eta")));
Deno.serve(app.fetch);
Now, when the application is running, we see that a GET request to /todos
returns the form.
curl localhost:8000/todos
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
</body>
</html>
Refactoring to controller
Keeping the layered architecture in mind, let's move the controller functionality to the todoController.js
. This includes moving the Eta import, Eta configuration, and the functionality for rendering the todos.eta
. Let's wrap the functionality for rendering the todos.eta
into a function called showForm
.
After refactoring, the todoController.js
looks as follows.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const showForm = (c) => c.html(eta.render("todos.eta"));
export { showForm };
Now, we can export the showForm
function from the todoController.js
and import it to the app.js
. After refactoring, 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);
Deno.serve(app.fetch);
After refactoring, the response is still as expected.
curl localhost:8000/todos
<!DOCTYPE html>
<html>
<head>
<title>TODO</title>
</head>
<body>
<form method="POST" action="/todos">
<label for="todo">Todo:</label>
<input type="text" id="todo" name="todo" /><br/>
<input type="submit" value="Add" />
</form>
</body>
</html>
Creating a todo on POST request
A todo entry should be created when the form is submitted. To create a todo entry, we need a route that responds to POST requests to /todos
, a controller function that handles the POST request, and a service function that stores the todo entry to the database.
Logging submitted form data
Let's start with the controller function. Let's call the function createTodo
. In the first version, the function simply logs the submitted form data to the console and uses the POST/Redirect/GET pattern to redirect the user back to the form.
The function is implemented as follows (note that the function is also added to the list of exported functions).
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const showForm = (c) => c.html(eta.render("todos.eta"));
const createTodo = async (c) => { const body = await c.req.parseBody(); conmsole.log(body); return c.redirect("/todos");};
export { createTodo, showForm };
Now, we can add a route to the app.js
that responds to POST requests to /todos
with the createTodo
function.
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.post("/todos", todoController.createTodo);
Deno.serve(app.fetch);
Now, with the server running, we can submit the form and see that we receive a redirect response.
curl -v -X POST -d "todo=Rest" localhost:8000/todos
...
< HTTP/1.1 302 Found
< location: /todos
...
%
With the above form data, we also see that the submitted form data is logged to the server console.
Listening on http://localhost:8000/
{ todo: "Rest" }
Functionality for storing submitted data to database
Now that we see that we receive the data, let's next add the functionality for storing the data to the database. To make it easier to remove and modify todos later on, we'll add an identifier to each todo entry that makes it easier to identify the entry. For each new todo, we'll use the crypto.randomUUID() method for creating a universally unique identifier (UUID version 4) string that is used as the identifier and the key.
UUIDs are a common way of identifying resources. They are 128-bit numbers that are designed for being unique without central coordination (i.e. a service that would keep track of which identifier to assign next). UUIDs are standardized in RFC 4122.
In essence, before storing the data to the database, we add an identifier to the object. After this, we open a connection to the database and save the todo to a collection called "todos".
As a whole, this functionality looks as follows and is implemented to the todoService.js
.
const createTodo = async (todo) => {
todo.id = crypto.randomUUID();
const kv = await Deno.openKv();
await kv.set(["todos", todo.id], todo);
};
export { createTodo };
Note that above, we assume that todo is an object -- i.e., the submitted form data that is an object with a single property called todo
.
Linking controller with service
Now that we have the functionality for storing data to the database in todoService.js
, it is time to link that functionality to the controller. To do this, we import the functionality from the todoService.js
to the todoController.js
, and call the function createTodo
from todoService.js
within the createTodo
of todoController.js
.
Concretely, this looks 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 = (c) => c.html(eta.render("todos.eta"));
const createTodo = async (c) => {
const body = await c.req.parseBody();
await todoService.createTodo(body);
return c.redirect("/todos");
};
export { createTodo, showForm };
Now, when we again submit the form to the server, we see a redirect response.
curl -v -X POST -d "todo=Rest" localhost:8000/todos
...
< HTTP/1.1 302 Found
< location: /todos
...
%
Our hope is that the data is stored to the database -- at least the application didn't crash. Next, we'll add functionality for reading the todos to make sure that the todos are actually added to the database.