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.