Key-Value Stores
Learning objectives
- You know what key-value stores are and you know of Deno KV.
- You know how to use a key-value store in a web application.
In the previous example, the data was stored in memory on the server. This means that whenever the server is restarted, the data is lost. To persist data -- that is, to store it in a way that it doesn't disappear between server restarts -- we need to store the data in a physical format (i.e. on a hard-drive). Here is where databases come into play.
Key-value stores are a type of database that store data in a key-value format. This means that each piece of data is stored with a key, and that key can be used to retrieve the data. For example, if we wanted to store a user's name, we could use the key name
and the value John Doe
. Then, when we want to retrieve the user's name, we can use the key name
to get the value John Doe
.
When working with key-value stores (and more any functionality that involves waiting for resources), we typically utilize asynchronous functions. Asynchronous functions allow waiting for the execution of the function, while potentially providing other processes the possibility to execute in the meanwhile. In JavaScript, the keyword async
is used to define asynchronous functions, and the keyword await
is used for waiting for the execution of an asynchronous function.
Deno KV
Deno comes with a built-in key-value store. The key-value store can be accessed using the asynchronous function Deno.openKv()
, which returns an object that can be used to access the key-value store. The relevant methods for working with the key-value store are get
, set
, and delete
-- all of them are asynchronous.
Deno KV is a new feature of Deno and can be accessed with the
--unstable
flag.
When running applications that use Deno KV, we use the command deno run --allow-net --unstable --watch
, followed by the name of the file we run.
Retrieving a value
The get
method is used for retrieving a value. It takes a list with a string as a parameter. The string is the key that is used. The following example outlines opening the key value store and retrieving the value for the key count
.
const kv = await Deno.openKv();
const count = await kv.get(["count"]);
The value returned by get
is an object, which contains a property value
that has the actual value stored into the key value database.
Assuming that a count exists, we could log it into the console as follows.
const kv = await Deno.openKv();
const count = await kv.get(["count"]);
console.log(count.value);
In practice, however, it is possible that the value is not found. In this case, the value within the returned object is null. Falling for a default value -- say 0 -- can be done with the ??
operator.
const kv = await Deno.openKv();
const count = await kv.get(["count"]);
let value = count.value ?? 0;
console.log(value);
Setting a value
The set
method is used for setting a value. It takes two parameters: (1) a list containing a string that indicates the name of the key, and (2) the value that is to be stored. The following example outlines opening the key value store and setting the value 0
for the key count
.
const kv = await Deno.openKv();
await kv.set(["count"], 0);
If the key-value storage already contains a value for the given key pair, the value will be overridden with the new value.
Deleting a value
The delete
method is used for deleting a value. Similar to get
, it takes a list with a string as a parameter, where the string is the key. The following example outlines the use of the delete
method. The example would remove the value for the key count
.
const kv = await Deno.openKv();
await kv.delete(["count"]);
A list with a single string?
The list that is given as a parameter to the get
, set
, and delete
methods can contain more than a single value, and the values can also be other than strings (e.g. numbers).
For additional information, check out the Deno KV documentation.
Getting and setting a value
When working with Deno KV, it is meaningful to create separate functions for working with the database. As an example, we likely would wish to have a separate function for retrieving the value and a separate function for setting the value. These could be called, for example, getValue
and setValue
.
They would be implemented as follows.
const getCount = async () => {
const kv = await Deno.openKv();
const count = await kv.get(["count"]);
return count.value ?? 0;
}
const setCount = async (count) => {
const kv = await Deno.openKv();
await kv.set(["count"], count);
}
With this functionality, we could adjust our earlier countService.js
to use the above functions. Now, instead of using a variable for storing the count, we would use Deno KV for storing the count. The following outlines the use of getCount
and setCount
as a part of the incrementCount
functionality.
const getCount = async () => {
const kv = await Deno.openKv();
const count = await kv.get(["count"]);
return count.value ?? 0;
}
const incrementCount = async () => {
let count = await getCount();
count++;
await setCount(count);
};
const setCount = async (count) => {
const kv = await Deno.openKv();
await kv.set(["count"], count);
}
export { getCount, incrementCount };
Note that the setCount
function is not exported from the countService.js
, as it is not needed outside of the file.
Deno KV in a web application
Using the above countService.js
, we can modify our earlier web application so that it uses the key-value store. The following application responds with the count to GET requests, while POST requests increment the count by one, returning the new count.
Although we previously mentioned that we would not have to change the implementation, we do need to change
app.js
a bit. This is because we changedcountService.js
so that the functions are asynchronous. Thus, we need to use asynchronous functions to access the functionality.
Note that as the getCount
and setCount
are asynchronous functions, we need to also define the functions that use them as asynchronous to allow the use of await
. Due to this, the functions that are mapped to routes are defined as async
.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
import * as countService from "./countService.js";
const app = new Hono();
app.get("/", async (c) => c.text(await countService.getCount()));
app.post("/", async (c) => {
await countService.incrementCount();
return c.text(await countService.getCount());
});
Deno.serve(app.fetch);
Now, when we launch the application with the command deno run --allow-net --unstable --watch app.js
-- assuming that the source code has been stored to a file called app.js
-- the application works as follows.
curl localhost:8000
0%
curl -X POST localhost:8000
1%
curl -X POST localhost:8000
2%
When you restart the application, the count starts at the number that it was left off previously.
curl localhost:8000
2%
Where is the data?
When using Deno KV locally, the data is stored in a SQLite database. The location of Deno files can be found using the command deno info
. The database location is under the "Origin storage".
When using Deno KV in the cloud, it automatically utilizes an edge ready database. We look into this in a bit more detail later on in the course, while additional emphasis on scalability is put on in the Designing and Building Scalable Web Applications course.