Building Restful APIs with Deno and Oak

With a simple book API, learn how to define middleware in Oak, handle request validation, create route handlers, and perform basic DB operations with DenoKV.


This content originally appeared on Telerik Blogs and was authored by Christian Nwamba

With a simple book API, learn how to define middleware in Oak, handle request validation, create route handlers and perform basic DB operations with DenoKV.

This article will guide you through creating a REST API using Deno, the Oak framework and a DenoKV database. We will build a simple book API that shows the different ways to define middleware in Oak, handle request validation, create route handlers and perform basic database operations with DenoKV.

What Is Deno?

Deno fixes many problems developers face with Node. Its straightforward approach helps developers write more secure, efficient, modern JavaScript code. One of its major selling points is security. By default, Deno does not allow access to the file system, network or environment variables unless explicitly allowed by the developer.

Deno also gives developers native TypeScript support without needing additional configuration.

What Is Oak?

Oak is a middleware framework for Deno that helps developers build web apps and APIs. It provides tools for handling HTTP requests, managing routing and integrating middleware, similar to Express in Node.js. It comes with TypeScript right out of the box and benefits from Deno’s security and modern runtime environment.

What Is DenoKV?

DenoKV is a key-value database that manages structured data for Deno.
Each piece of data or “value” has a unique identifier or “key,” making it easy to fetch data by referencing its key. This approach allows developers to manage data without setting up a separate database server.

Project Setup

Run the following command to install deno for macOS using Shell:

curl -fsSL https://deno.land/install.sh | sh

For Windows using PowerShell:

irm https://deno.land/install.ps1 | iex

For Linux using Shell:

curl -fsSL https://deno.land/install.sh | sh

To test your installation, run the following command:

 deno -version

Create New Project

Next, we need to create a new project. Run the command deno init deno-oak-demo to create a new project called deno-oak-demo, then cd into it.

Initializing a new Deno project using bash

Next, we need to create three new files in the deno-oak-demo directory called book.routes.ts, book.types.ts and validation.ts.

Your deno-oak-demo directory should now look like this.

Project directory

Install Deno VS Code Extension

We need to install Deno’s official VS Code extension. This extension adds support for Deno, such as offering import suggestions and automatically caching remote modules.

Deno’s official VSCode extension

Install Oak

We’ll use JSR to install Oak. JSR, or JavaScript Repository, is a package registry designed by the creators of Deno. It allows developers to publish their TypeScript code directly without the need to transpile it first. Its key advantage is that it supports ES Modules only and is TypeScript-first.

We’ll use the command deno add jsr:**@oak**/oak to install the Oak framework as a dependency. If this is successful, a deno.lock file will be created.

The deno.lock file helps prevent unexpected changes in dependencies during the life of your application by locking specific dependency versions.

Your deno.json file should now look like this.

Image showing deno.json file

When we install a package in Deno, it is added to the deno.json file as an import. If we want to import this package into a file, we can use the alias defined in the deno.json or directly reference the package’s URL.

Registering Middleware in Oak

Middleware functions are processing layers that handle requests and responses in our application.

An Oak application is a chain of various middleware functions, such as route handlers, validation functions, custom error handlers and loggers. We can register a middleware on the application as a whole, a group of routes(a router) or a specific route.

This is how to register a middleware on the Application as a whole:

import { Application, Context, Next } from "@oak/oak";
const app = new Application();
const firstMiddleware = async (context: Context, next: Next) => {
  console.log("Running first Middleware");
  await next();
};
app.use(firstMiddleware);
app.use((context) => {
  console.log("Running second Middleware");
  context.response.body = "Hello world!";
});

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

In the example above, we first define a middleware function and then register it to the application as a whole using the app.use method.

Notice that the middleware function has two parameters, context and next. context is an object used to access the request data and response methods, while next is a function used to call the next middleware in the chain.

In the example above, the first middleware will be called, thereby logging “Running first Middleware” to the console. This will be followed by the second middleware, which logs “Running second Middleware” to the console and returns “Hello world!” in the response body.

It’s important to note that the order in which middleware is registered is important.

Take a look at the code snippet below.

import { Application, Context, Next } from "@oak/oak";
const app = new Application();
app.use((context) => {
  console.log("Running second Middleware");
  context.response.body = "Hello world!";
});
const firstMiddleware = async (context: Context, next: Next) => {
  console.log("Running first Middleware");
  await next();
};
app.use(firstMiddleware);

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

In this example, only “Running second Middleware” will be logged to the console, and “Hello world” will be sent as the response body. This is because after running the first middleware that appears in the chain, we didn’t add a next() function call.

This is how to register a middleware on a router:

import { Application, Context, Next, Router } from "@oak/oak";
const app = new Application();

const router = new Router();
router.prefix("/greeting");

const firstMiddleware = async (context: Context, next: Next) => {
  console.log("Running first Middleware");
  await next();
};
router.use(firstMiddleware);
router.get("/one", (context) => {
  context.response.body = "Hello, World!";
});
router.get("/two", (context) => {
  context.response.body = "What's Up, World!";
});

app.use(router.routes());
app.use(router.allowedMethods());

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

In the above example, we define a new router, set the prefix for all its routes, register a middleware function and create two specific route handlers.

Since our middleware function is registered above the two route handlers, if either “/greeting/one” or “/greeting/two” is hit, our middleware function will run before the greeting is sent as the response body.

This is how to register a middleware on a specific route.

import { Application, Context, Next, Router } from "@oak/oak";
const app = new Application();

const router = new Router();
router.prefix("/greeting");

const firstMiddleware = async (context: Context, next: Next) => {
  console.log("Running first Middleware");
  await next();
};
router.get("/one", firstMiddleware, (context) => {
  context.response.body = "Hello, World!";
});
router.get("/two", (context) => {
  context.response.body = "What's Up, World!";
});

app.use(router.routes());
app.use(router.allowedMethods());

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

In the above example, our middleware function will only run when “greeting/one” is hit.

Request Validation

Now that we know how middleware works in Oak, let’s build our REST API. First, we’ll create a middleware function to handle request validation. We’ll use this function later when adding route handlers to create and update books.

Deno supports importing npm packages using the npm: specifier. We’ll import a package called Joi from NPM.

Add the following code to your validation.ts file:

import { Context, Next } from "@oak/oak";
import Joi, { ObjectSchema } from "npm:joi";

export const createBookSchema = Joi.object({
  title: Joi.string().required(),
  author: Joi.string().required(),
  description: Joi.string().required(),
});

export const updateBookSchema = Joi.object({
  title: Joi.string().optional(),
  author: Joi.string().optional(),
  description: Joi.string().optional(),
}).or("title", "author", "description");

export const validate =
  (schema: ObjectSchema) => async (context: Context, next: Next) => {
    const body = await context.request.body.json();
    const { error } = schema.validate(body, { abortEarly: false });

    if (error) {
      context.response.status = 400;
      context.response.body = {
        errors: error.details.map((d) => d.message),
      };
    } else {
      await next();
    }
  };

Defining Types

Next, in the book.types.ts file, let’s define a Book type with an id, title, author and description.

export interface Book {
  id: string;
  title: string;
  author: string;
  description: string;
}

Configure Book Router

Next, let’s import the Oak Router, Book interface, createBookSchema and updateBookSchema into the book.routes.ts file:

import { Router } from "@oak/oak/router";
import type { Book } from "./book.types.ts";
import { createBookSchema, updateBookSchema, validate } from "./validation.ts";

Next, initialize the DenoKV database, create a bookRouter and set its prefix to “/books”:

const kv = await Deno.openKv();
const bookRouter = new Router();
bookRouter.prefix("/books");

Next, create a helper function to get a book by its ID:

async function getBookById(id: string) {
  const entry = await kv.get(["books", id]);
  return entry.value as Book | null;
}

In the snippet above, kv.get takes an array with two strings: one represents the namespace “books” and the other represents the key for the specific book to be retrieved.

Next, let’s define the route handler to get a book by ID:

bookRouter.get("/:id", async (context) => {
  try {
    const id = context.params.id;
    const book = await getBookById(id);

    if (book) {
      context.response.body = book;
    } else {
      context.response.status = 404;
      context.response.body = { message: "Book not found" };
    }
  } catch (error) {
    console.log(error);
    context.response.status = 500;
    context.response.body = { message: "Failed to retrieve book" };
  }
});

Next, let’s add a route handler to get all books:

bookRouter.get("/", async (context) => {
  try {
    const entries = kv.list({ prefix: ["books"] });
    const books: Book[] = [];

    for await (const entry of entries) {
      books.push(entry.value as Book);
    }

    context.response.body = books;
  } catch (error) {
    console.log(error);
    context.response.status = 500;
    context.response.body = { message: "Failed to fetch books" };
  }
});

In the snippet above, kv.list retrieves all key-value pairs that share a common prefix (namespace).

Next, add the route handler to create a new book:

bookRouter.post("/", validate(createBookSchema), async (context) => {
  try {
    const body = await context.request.body.json();

    const uuid = crypto.randomUUID();
    const newBook: Book = { id: uuid, ...body };

    await kv.set(["books", uuid], newBook);

    context.response.status = 201;
    context.response.body = { message: "Book added", book: newBook };
  } catch (error) {
    console.log(error);
    context.response.status = 500;
    context.response.body = { message: "Failed to add book" };
  }
});

In the snippet above, kv.set can be used to save a new key-value pair in the database. In this case, it takes two parameters: an array with two strings (the namespace “books” and the key uuid) and the value to be saved (newBook).

Next, let’s add the route handler to update a book by ID:

bookRouter.patch("/:id", validate(updateBookSchema), async (context) => {
  try {
    const id = context.params.id;
    const existingBook = await getBookById(id);

    if (!existingBook) {
      context.response.status = 404;
      context.response.body = { message: "Book not found" };
      return;
    }

    const body = await context.request.body.json();

    const updatedBook = { ...existingBook, ...body };

    await kv.set(["books", id], updatedBook);
    context.response.status = 200;
    context.response.body = { message: "Book updated", book: updatedBook };
  } catch (error) {
    console.log(error);
    context.response.status = 500;
    context.response.body = { message: "Failed to update book" };
  }
});

In the snippet above, kv.set can also be used to update the value of a key-value pair. In this case, it takes two arguments:

  1. An array with two strings: the namespace “books” and the key whose value will be the updated ID.
  2. The new value to be updated (updatedBook).

Finally, let’s add the route handler to delete a book by ID and export bookRouter:

bookRouter.delete("/:id", async (context) => {
  try {
    const id = context.params.id;
    const book = await getBookById(id);

    if (!book) {
      context.response.status = 404;
      context.response.body = { message: "Book not found" };
      return;
    }

    await kv.delete(["books", id]);
    context.response.status = 200;
    context.response.body = { message: "Book deleted", book };
  } catch (error) {
    console.log(error);
    context.response.status = 500;
    context.response.body = { message: "Failed to delete book" };
  }
});

export { bookRouter };

In the snippet above, kv.delete is used to delete a given key-value pair.

Initialize Oak Application

Replace the code in your main.ts file with the following:

import { Application } from "@oak/oak/application";
import { bookRouter } from "./book.routes.ts";

const app = new Application();

app.use(bookRouter.routes());
app.use(bookRouter.allowedMethods());

app.listen({ port: 3000 });

Finally, we need to make a small change to our deno.json file to run our app. Add the --unstable-kv and --allow-net flags to dev task.

Aside from your version of Oak and the assert library, your deno.json should now look like this.

{
"tasks": {
"dev": "deno run --watch --unstable-kv --allow-net main.ts"
},
"imports": {
"@oak/oak": "jsr:@oak/oak@^17.1.3",
"@std/assert": "jsr:@std/assert@1"
}
}

We added the --stable-kv flag because DenoKV is still an unstable API. We also added the --allow-net flag to grant main.ts access to the internet.

With this in place, we can start our app by running the command deno run dev.

Conclusion

In this guide, we built a simple book API that shows how to define middleware in Oak, handle request validation, create route handlers and perform basic DB operations with DenoKV.


This content originally appeared on Telerik Blogs and was authored by Christian Nwamba


Print Share Comment Cite Upload Translate Updates
APA

Christian Nwamba | Sciencx (2025-01-17T15:14:58+00:00) Building Restful APIs with Deno and Oak. Retrieved from https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/

MLA
" » Building Restful APIs with Deno and Oak." Christian Nwamba | Sciencx - Friday January 17, 2025, https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/
HARVARD
Christian Nwamba | Sciencx Friday January 17, 2025 » Building Restful APIs with Deno and Oak., viewed ,<https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/>
VANCOUVER
Christian Nwamba | Sciencx - » Building Restful APIs with Deno and Oak. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/
CHICAGO
" » Building Restful APIs with Deno and Oak." Christian Nwamba | Sciencx - Accessed . https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/
IEEE
" » Building Restful APIs with Deno and Oak." Christian Nwamba | Sciencx [Online]. Available: https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/. [Accessed: ]
rf:citation
» Building Restful APIs with Deno and Oak | Christian Nwamba | Sciencx | https://www.scien.cx/2025/01/17/building-restful-apis-with-deno-and-oak/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.