Key-value Databases and Deno
Learning Objectives
- You know of key-value databases and you know what Deno KV is.
- You know how to use Deno KV to store and retrieve data.
- You know how to use Deno KV in a web application.
Key-value databases 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 the key can be used to retrieve the data. For example, if we wanted to store information of a user, we could use a unique identifier such as an email address as the key, and the user’s information as the value. When we want to retrieve the user’s information, we can use the email address to get the user’s information.
Deno comes with a built-in key-value database called Deno KV. The key-value database can be accessed using the asynchronous function Deno.openKv()
, which returns an object that can be used to access the key-value database. The relevant methods for working with the key-value database are get
, set
, and delete
— all of them are asynchronous.
Deno KV is a new feature that is available with the
--unstable-kv
flag.
When running applications that use Deno KV, we use the command deno run --allow-net --unstable-kv --watch
, followed by the name of the file we run. For the walking skeleton, this means that the last line of the Dockerfile
in the server
directory would have to be as follows.
CMD [ "run", "--allow-env", "--allow-net", "--unstable-kv", "--watch", "app-run.js" ]
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 Deploy to host the application, Deno automatically utilizes a database when using Deno KV. Information about the database can be found in the Deno Deploy dashboard of the project.
Using Deno KV
Deno KV has four main methods for working with the key-value database:
- The method
get
is used to retrieve a value. - The method
set
is used to set a value. - The method
list
is used to retrieve a list of values. - The method
delete
is used to remove a value.
All of the methods are asynchronous and return promises.
Retrieving a value
The get
method is used for retrieving a value. It takes a list as a parameter where the values in the list act as the key. The following example outlines opening the database and retrieving the value for the key ["statistics", "visits"]
.
const kv = await Deno.openKv();
const visits = await kv.get(["statistics", "visits"]);
The value returned by get
is an object, which contains a property value
that has the actual value from the database.
Assuming that a value for the key exists, we can log the value into the console as follows.
const kv = await Deno.openKv();
const visits = await kv.get(["statistics", "visits"]);
console.log(count.value);
It is also possible that no value is found for a given key. 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 visits = await kv.get(["statistics", "visits"]);
let value = visits.value ?? 0;
console.log(value);
Setting a value
The set
method is used for setting a value. It takes two parameters: (1) a list where the values in the list act as 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 ["statistics", "visits"]
.
const kv = await Deno.openKv();
await kv.set(["statistics", "visits"], 0);
If the key-value database already contains a value for the given key pair, the value will be overridden with the new value.
Listing values
The list
method is used to retrieve values that match a given prefix. The method takes an object as a parameter, where the object can contain a property prefix
that is a list. The following example outlines the use of the list
method. The example would retrieve all values that have a prefix my
and add them to the list result
.
const records = kv.list({ prefix: ["statistics"] });
const result = [];
for await (const res of records) {
result.push(res.value);
}
console.log(result);
Note that as the list
method returns an asynchronous iterator, we need to use a for await
loop to iterate over the values.
Deleting a value
The delete
method is used for deleting a value. Similar to get
, it takes a list where the values act as the key as a parameter. The following example outlines the use of the delete
method. The example would remove the value for the key ["statistics", "visits"]
.
const kv = await Deno.openKv();
await kv.delete(["statistics", "visits"]);
Deno KV and Repository Pattern
When discussing the repository and CRUD patterns, we created a bookRepository.js
that used Postgres.js to access a PostgreSQL database.
The repository pattern allows us to change the data access without changing the rest of the application.
The functions that the bookRepository.js
file contained were create
, readAll
, readOne
, update
, and remove
. Without the internal implementations, the file looked as follows.
const create = async (book) => {
// Create a new book item
};
const readAll = async () => {
// Retrieve all book items
};
const readOne = async (id) => {
// Retrieve a book item with the given id
};
const update = async (id, book) => {
// Update a book item with the given id
};
const remove = async (id) => {
// Delete a book item with the given id
};
export { create, readAll, readOne, update, remove };
If we would wish to change the implementation to use Deno KV instead of PostgreSQL, we would have to rewrite the functions to use the key-value database. The create
function would have to use the set
method, the readAll
function would have to use the list
method, the readOne
function would have to use the get
method, the update
function would have to use the set
method, and the remove
function would have to use the delete
method.
In addition, we would have to do some parsing of the data, as e.g. the list
method returns an asynchronous iterator, and we would have to iterate over to get the actual values.
Creating a book
The create
function would have to use the set
method to store the book. As Deno KV does not create identifiers automatically, we would have to set an identifier for the book ourselves — as an example, the crypto.randomUUID()
function could be used to create a random identifier.
const create = async (book) => {
const kv = await Deno.openKv();
book.id = crypto.randomUUID();
await kv.set(["books", book.id], book);
return book;
};
Reading all books
To read all books, we would have to use the list
method to retrieve all books that have a prefix books
. As the list
method returns an asynchronous iterator, we would have to iterate over the values to get the actual books.
const readAll = async () => {
const kv = await Deno.openKv();
const records = kv.list({ prefix: ["books"] });
const result = [];
for await (const record of records) {
result.push(record.value);
}
return result;
};
With these two changes in place, and with the API from the chapter on repository and crud pattern we could already try creating a book and listing the books.
Now, posting a book to the API would create a book in the key-value database and return the book with the identifier.
curl -X POST -d '{"title": "The Hobbit", "year": 1937}' localhost:8000/books
{"title":"The Hobbit","year":1937,"id":"23daf1fa-ffc4-49b0-a460-adb5a15d4e47"}%
Once the book has been created, we can also as for a list of books.
curl localhost:8000/books
[{"title":"The Hobbit","year":1937,"id":"23daf1fa-ffc4-49b0-a460-adb5a15d4e47"}]%
Reading one book
To read one book, we would have to use the get
method to retrieve the book with the given identifier.
const readOne = async (id) => {
const kv = await Deno.openKv();
const book = await kv.get(["books", id]);
return book.value;
};
With this function in place, we could now also ask for a single book.
curl localhost:8000/books/23daf1fa-ffc4-49b0-a460-adb5a15d4e47
{"title":"The Hobbit","year":1937,"id":"23daf1fa-ffc4-49b0-a460-adb5a15d4e47"}%
Updating a book
To update a book, we would have to use the set
method to update the book with the given identifier.
const update = async (id, book) => {
const kv = await Deno.openKv();
book.id = id;
await kv.set(["books", id], book);
return book;
};
Removing a book
To remove a book, we would have to use the delete
method to remove the book with the given identifier. However, our earlier implementation of the delete
function also returned the book that was removed. To do this, we would first have to retrieve the book and then remove it. This would work as follows.
const remove = async (id) => {
const book = await readOne(id);
const kv = await Deno.openKv();
await kv.delete(["books", id]);
return book;
};
With these changes in place, we could also update and remove a book. Updating the book would be done with the HTTP PUT method as follows.
curl -X PUT -d '{"title": "The Lord of the Rings", "year": 1954}' localhost:8000/books/23daf1fa-ffc4-49b0-a460-adb5a15d4e47
{"title":"The Lord of the Rings","year":1954,"id":"23daf1fa-ffc4-49b0-a460-adb5a15d4e47"}%
Similarly, removing a book would be done with the HTTP DELETE method.
curl -X DELETE localhost:8000/books/23daf1fa-ffc4-49b0-a460-adb5a15d4e47
{"title":"The Lord of the Rings","year":1954,"id":"23daf1fa-ffc4-49b0-a460-adb5a15d4e47"}%
Now, after the changes, the database would be empty, and asking for a list of books would return an empty list.
curl localhost:8000/books
[]%
Deno KV book repository
To summarize, the book repository that uses Deno KV would look as follows.
import postgres from "postgres";
const sql = postgres();
const create = async (book) => {
const kv = await Deno.openKv();
book.id = crypto.randomUUID();
await kv.set(["books", book.id], book);
return book;
};
const readAll = async () => {
const kv = await Deno.openKv();
const records = kv.list({ prefix: ["books"] });
const result = [];
for await (const record of records) {
result.push(record.value);
}
return result;
};
const readOne = async (id) => {
const kv = await Deno.openKv();
const book = await kv.get(["books", id]);
return book.value;
};
const update = async (id, book) => {
const kv = await Deno.openKv();
book.id = id;
await kv.set(["books", id], book);
return book;
};
const remove = async (id) => {
const book = await readOne(id);
const kv = await Deno.openKv();
await kv.delete(["books", id]);
return book;
};
export { create, readAll, readOne, remove, update };