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 testand 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.