Form Data in Deno KV
Learning objectives
- You know how to store submitted form data using Deno KV.
- You know how to retrieve a list of data from Deno KV.
Although we've seen how to store submitted form data in a variable, this is not a good solution for storing data in the long term. As we know, if the server is restarted, the data is lost. We previously briefly looked into working with Deno KV -- let's now use it to store submitted form data.
Opening a connection and storing an address
The key functionality for storing data in Deno KV relates to creating a connection to the database -- with the call Deno.openKv
-- and setting a value with the method set
. The method set
takes two parameters, a list and a value. The list contains the key and the subkey, while the value is the data that is stored in the database. In the case of our address book, we could use the string "addresses" as the key and the name as the subkey, while the data that would be stored would be an entry containing both the name and the address.
For this, it would be meaningful to create a separate file addressService.js
, which is responsible for interacting with Deno KV. The following example shows how to store the submitted data in Deno KV.
const addAddress = async (addressData) => {
const kv = await Deno.openKv();
await kv.set(["addresses", addressData.name], addressData);
};
export { addAddress };
Now, we can adjust our application to use the address service. Let's first just import the address service and call the addAddress
method with the submitted data, without changing the existing functionality.
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";
import * as addressService from "./addressService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const app = new Hono();
let data = {};
app.get("/", (c) => c.html(eta.render("index.eta", data)));
app.post("/addresses", async (c) => {
const body = await c.req.parseBody();
await addressService.addAddress(body); data = body;
return c.html(eta.render("index.eta", data));
});
Deno.serve(app.fetch);
Listing addresses
Our earlier application was a bit silly. We were simply storing the previously submitted data in a variable, and showing it to the user. Let's expand on this and instead of showing a single address, show all the addresses that have been stored so far. For this, we need to modify the Eta template to show all the addresses. The following example shows how this can be done -- we use a table for listing the addresses, given that there are addresses.
<!DOCTYPE html>
<html>
<head>
<title>Hello forms!</title>
</head>
<body>
<form method="POST" action="/addresses">
<label for="name">Name:</label>
<input type="text" id="name" name="name" /><br/>
<label for="address">Address:</label>
<input type="text" id="address" name="address" /><br/>
<input type="submit" value="Submit form" />
</form>
<% if (it && it.addresses) { %> <p>Addresses:</p> <table> <tr> <th>Name</th> <th>Address</th> </tr> <% it.addresses.forEach((entry) => { %> <tr> <td><%= entry.name %></td> <td><%= entry.address %></td> </tr> <% }); %> </table> <% } %> </body>
</html>
The next part would be to modify the addressService.js
so that it can be used to retrieve the addresses from the database. For this, we need a function listAddresses
that returns all the addresses that have been stored so far. Here, Deno KV's list method is useful -- we can use it to retrieve all the data under a specific key
. The returned data is an iterator, which we need to then iterate over -- conrete entries are stored in the value
property of each entry.
The following example shows how the list
method is used for retrieving the addresses and adding them to a list.
const addAddress = async (addressData) => {
const kv = await Deno.openKv();
await kv.set(["addresses", addressData.name], addressData);
};
const listAddresses = async () => {
const kv = await Deno.openKv();
const addressData = kv.list({ prefix: ["addresses"] }); const addresses = []; for await (const entry of addressData) { if (entry != null && entry.value != null) { addresses.push(entry.value); } }
return addresses;
};
export { addAddress, listAddresses };
Now, we can modify the app.js
to use the listAddresses
function.
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";
import * as addressService from "./addressService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const app = new Hono();
app.get(
"/",
async (c) => {
const data = {
addresses: await addressService.listAddresses(),
};
return c.html(
eta.render("index.eta", data),
);
},
);
app.post("/addresses", async (c) => {
const body = await c.req.parseBody();
await addressService.addAddress(body);
const data = {
addresses: await addressService.listAddresses(),
};
return c.html(
eta.render("index.eta", data),
);
});
Deno.serve(app.fetch);
Now, when we open up the application, we see the existing list of addresses. Similarly, when we add an address, it is added to the list.
Minor refactoring
In the case of the above example, the app.js
would grow a bit large, and it would be meaningful to divide it into smaller parts. Here, a logical division would be to introduce a file called addressController.js
, which would be responsible for handling the requests.
One possibility of how the addressController.js
could look like is as follows -- note that Eta is also used in the address controller, and thus it needs to be imported there.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
import * as addressService from "./addressService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const getAndRenderAddresses = async (c) => {
const data = {
addresses: await addressService.listAddresses(),
};
return c.html(
eta.render("index.eta", data),
);
};
const listAddresses = async (c) => {
return await getAndRenderAddresses(c);
};
const addAddressAndListAddresses = async (c) => {
const body = await c.req.parseBody();
await addressService.addAddress(body);
return await getAndRenderAddresses(c);
};
export { addAddressAndListAddresses, listAddresses };
Now, our app.js
would be quite a bit simpler, as it would only be responsible for creating the server and routing the requests to the address controller.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
import * as addressController from "./addressController.js";
const app = new Hono();
app.get("/", addressController.listAddresses);
app.post("/addresses", addressController.addAddressAndListAddresses);
Deno.serve(app.fetch);
When we consider our application, it is a bit weird that the addresses are retrieved and rendered for both GET and POST requests. There's also a small side-effect -- when we press F5
to refresh the page where we submitted the form, the browser submits the form data again. Let's look into addressing this next.