Dejan Lukić

How to Create a CRUD API Using Deno KV

Originally written for Deno.

Concepts

Overview

oak is a middleware framework for Deno’s native HTTP server. It will serve the API routes and handle the authentication.

Deno KV is a key-value database that can be used to store and retrieve data. Data is stored as key-value pairs. Read a detailed overview of Deno KV.

Web server

This web server will handle routes, authentication, CORS and session.

Start by importing the dependencies:

main.ts

import { Application } from 'https://deno.land/x/oak@v11.1.0/mod.ts';
import { oakCors } from 'https://deno.land/x/cors@v1.2.2/mod.ts';

Next, construct a new Application() class. In oak, Application class coordinates managing the HTTP server, running middleware, handling errors and processing requests.

You might be familiar with const app = express() in Node.js - it’s the similar fashion here in oak.

const app = new Application();

To allow requests from different origins (e.g. call from frontend), provide the Cross-Origin Resource Sharing (CORS) middleware to the app.

CORS is a security mechanism implemented in web browsers to control access to resources on a different origin (a domain). It allows the backend API to specify which domains are allowed to access its resources, protecting against unauthorized cross-origin requests.

app.use(oakCors({}));

Now, create a new folder routes and create a file index.ts that will contain all the routes.

oak has a built-in router that handles all HTTP methods and requests. It allows you to provide middlewares, as well as allowing you to paramaterize parts of the requested path.

routes/index.ts

First off, import the necessary dependencies.

import { Context, Router } from 'https://deno.land/x/oak@v11.1.0/mod.ts';

Then, export the router with the app state.

export const router = new Router();

Create the first route that will send ‘Hello World’ when you navigate to ‘/’.

router.get('/', (ctx: Context) => {
	ctx.response.body = `Hello, world!`;
});

Back in the main.ts, you need to pass the router to the application

main.ts

import { router } from './routes/index.ts';
...
app.use(router.allowedMethods(), router.routes());

router.allowedMethods() is a middleware that handles requests for HTTP methods registered with the router.

If no routes handle a specific method, the router will employ the "not allowed" logic. In cases where a method is supported by certain routes but not by the specific matched router, the router will return "not implemented”. See more about allowedMethods.

router.routes will do all the route processing that the router has been configured to handle.

To start the app, you need to start the server listen process.

await app.listen({ port: 8000 });

The application method .listen() is used to open the server, begin listening for requests, and processing the registered middleware for each request. This method provides a promise that resolves when the server is closed.

Start the server by opening your terminal window and running the following command:

deno run --allow-net main.ts

The --allow-net flag allows Deno to communicate with the networking interfaces on your machine.

Implementing Deno KV

Deno KV is a key-value database that can be used to store and retrieve data.

Start by defining an interface for the data stored in the Deno KV. In this example, you will be making a blog CRUD demo.

Create a new folder shared , and inside of it a new file api.ts

shared/api.ts

export interface Post {
    name: string;
    content: string;
    author: string;
}

Next, create a new folder services , and inside of it a new file db.ts.

Start by impoting the Post interface

services/db.ts

import { Post } from '../shared/api.ts';

Open the connection to the Deno KV.

const kv = await Deno.openKv();

Now, you’re able to mutate the database with the kv method.

CRUD Methods

The CRUD (Cread, Read, Update, Delete) methods are fundamental operations in many software applications for interacting with a database or data storage.

Create a post method

To create a post, you need to create a function that will take the name, content and the author of the post as its arguments.

export const createPost = async ({ name, content, author }: Post) => {
	return await kv.set(['posts', name], { name, content, author });
};

In kv.set(), ['posts', <name>] is the key, and the object { name, content, author } will be assigned to the key in the Deno KV.

Get a specific post method

Getting a specific post follows the similar fashion:

export const getPost = async (name: string) => {
	return (await kv.get(['posts', name])).value;
};

To get all the posts in the Deno KV, you don’t need to provide any parameters, as the kv.list() method has a prefix property that will retrieve all the key-value pairs that start with the given prefix.

Get all posts method

export const getPosts = async () => {
	const posts = [];
	for await (const entry of kv.list({ prefix: ['posts'] })) {
		posts.push(entry.key);
	}
	return posts;
};

This function asynchronously retrieves a list of post keys from a key-value storage system with the prefix ['posts'], and then returns the list of keys.

Delete a post method

Delete a post with kv.delete() that takes the name of the post.

export const deletePost = async (name: string) => {
	return await kv.delete(['posts', name]);
};

To update a post, we will provide 3 parameters. The required parameter is the name, which serves as the identifier in the whole demo.

Update a post method

Other parameters are optional, since you won’t always change all parameters.

export const updatePost = async (
	name: string,
	content?: string,
	author?: string
) => {
	const post: any = await kv.get(['posts', name]);
	if (post) {
		post.name = name || post.name;
		post.content = content || post.content;
		post.author = author || post.author;
		return await kv.set(['posts', name], post);
	}
};

This code asynchronously updates a post in Deno KV by retrieving the post with the given name, modifying its properties (name, content, and/or author) if new values are provided, and then storing the updated post back into the storage system with the same key.

Implementing Deno KV in API routes

Now, you need to implement the Deno KV methods you created previously in the API.

Start by importing the dependencies:

import { createPost, deletePost, getPost, getPosts, updatePost } from '../services/db.ts';

Create post route

Start with the /create route. The route will take the name, content and author name from the request’s JSON body and pass it to createPost() method.

routes/index.ts

router.post('/create', async (ctx: Context) => {
		const { name, content, author } = await ctx.request.body().value;
		const post = await createPost({ name, content, author });
		ctx.response.body = { success: post.ok };
	})

The Context class in oak provides context about the current request and response to middleware functions.

Context.response is an object which contains information about the response that will be sent when the middleware finishes processing.

When the createPost() method finishes with the Deno KV mutation, you the ctx.response.body will be updated, and sent with the { success: post.ok } object.

Read a specific post route

To fetch a specific post, we need to provide a dynamic router parameter, which is prefixed with : in oak. In this case you will fetch the article by its name, and you need to access the name dynamically.

To do so, you will use a :name dynamic router parameter.

Context class will provide the URL parameters in the Context.params object, by which you can access the post name by simply using the following:

 router.get('/read/:name', async (ctx: Context) => {
		// get params from ctx
		const name = ctx.params.name;
		const post = await getPost(name);
		ctx.response.body = { success: !!post, data: post };
	})

The !! shorthand operator is used to convert a value to a a boolean. It can be used to explicitly coerce a value to either true or false.

Read posts route

To get all posts, you don’t need to provide any parameters. Simply call the getPosts() function to retrieve them.

router.get('/read', async (ctx: Context) => {
		const posts = await getPosts();
		ctx.response.body = { success: !!posts, data: posts };
	})

Update a post route

In this case, you will take both the URL parameters and the body JSON value to mutate the post in Deno KV.

ctx.params.name will again take the name of the route from the dynamic router parameter :name.

Destructuring the ctx.request.body().value will give you the content and author values.

router.put('/update/:name', async (ctx: Context) => {
		const name = ctx.params.name;
		const { content, author } = await ctx.request.body().value;
		const post = await updatePost(name, content, author);
		ctx.response.body = { success: post?.ok || false };
	})

Delete a post route

Take the :name dynamic router parameter and call the deletePost() method to delete the post. Upon deletion, the function will provide a boolean that will be returned in the response body.

router.delete('/delete/:name', async (ctx: any) => {
		const name = ctx.params.name;
		const post = await deletePost(name);
		ctx.response.body = { success: post };
	})

Test the demo

To test the demo, run the following command which will start the oak server:

deno run --allow-net --unstable main.ts

You need to provide the --unstable flag in the Deno CLI, because currently Deno KV is still an experimental feature in Deno and is subject to change.

#api #crud #deno #kv