What We'll Build

In this tutorial, we'll create a simple but realistic REST API for managing a list of books. By the end, you'll have endpoints to create, read, update, and delete records — all running on Deno with the Oak framework handling routing and middleware.

Why Oak?

Oak is inspired by Koa (a Node.js framework) and is one of the most popular HTTP middleware frameworks in the Deno ecosystem. It gives you a clean router API, middleware chaining, and context-based request/response handling that feels familiar to Express or Koa users.

Project Setup

Create a project folder and a main.ts entry file. We'll import Oak directly from the Deno module registry — no package manager needed.

// main.ts
import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const router = new Router();

Defining Your Data Model

For this example, we'll use an in-memory array as our data store:

interface Book {
  id: number;
  title: string;
  author: string;
}

let books: Book[] = [
  { id: 1, title: "The Pragmatic Programmer", author: "Hunt & Thomas" },
  { id: 2, title: "Clean Code", author: "Robert C. Martin" },
];

Setting Up Routes

GET all books

router.get("/books", (ctx) => {
  ctx.response.body = books;
});

GET a single book by ID

router.get("/books/:id", (ctx) => {
  const id = Number(ctx.params.id);
  const book = books.find((b) => b.id === id);
  if (book) {
    ctx.response.body = book;
  } else {
    ctx.response.status = 404;
    ctx.response.body = { error: "Book not found" };
  }
});

POST — create a new book

router.post("/books", async (ctx) => {
  const body = ctx.request.body({ type: "json" });
  const value = await body.value;
  const newBook: Book = { id: Date.now(), ...value };
  books.push(newBook);
  ctx.response.status = 201;
  ctx.response.body = newBook;
});

DELETE — remove a book

router.delete("/books/:id", (ctx) => {
  const id = Number(ctx.params.id);
  books = books.filter((b) => b.id !== id);
  ctx.response.status = 204;
});

Wiring It All Together

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

console.log("Server running on http://localhost:8000");
await app.listen({ port: 8000 });

Running the API

Because our API needs network access, run it with the appropriate permission flag:

deno run --allow-net main.ts

Test your endpoints using curl or a tool like Insomnia/Postman:

# Fetch all books
curl http://localhost:8000/books

# Create a new book
curl -X POST http://localhost:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Deno in Action","author":"You"}'

Adding Error-Handling Middleware

Wrap your routes with a global error handler for cleaner responses:

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = 500;
    ctx.response.body = { error: err.message };
  }
});

Place this before your router middleware so it catches any downstream errors.

Next Steps

  • Replace the in-memory store with a real database (e.g., Postgres via deno-postgres)
  • Add input validation using a schema library
  • Implement JWT-based authentication
  • Write tests using deno test and Oak's test utilities

Oak makes building Deno APIs intuitive. Its middleware pattern keeps code modular, and because it's built for Deno, you get the full benefit of TypeScript types and Deno's security model throughout your app.