How to Build a RESTful API with Deno and Oak

Deno is a secure runtime for JavaScript and TypeScript that has been gaining popularity as an alternative to Node.js. It offers several benefits such as better security defaults, a simpler module system, and first-class TypeScript support.

In this tutorial, we‘ll explore how to use Deno to build a RESTful API for a todo application. We‘ll leverage the Oak framework to handle routing and middleware. Oak is a lightweight and expressive framework inspired by Koa.

By the end of this post, you‘ll have learned how to:

  • Set up a new Deno project
  • Define routes for CRUD operations on todo items
  • Implement controllers to handle the business logic
  • Use TypeScript interfaces to model data
  • Test API endpoints using a REST client
  • Add logging and error handling with middleware

Let‘s jump right in!

Getting Started with Deno

First, make sure you have Deno installed. You can download an installer from the official Deno website:

https://deno.land/#installation

Once installed, you can verify the version by running:

deno --version  

This tutorial assumes you have Deno 1.0 or later.

Setting Up the Project

Create a new directory for the project and open it in your favorite code editor:

mkdir deno-todo-api
cd deno-todo-api
code .

Next, create a server.ts file. This will be the entry point for the application:

import { Application } from ‘https://deno.land/x/oak/mod.ts‘;

const app = new Application();
const PORT = 8000;

app.addEventListener(‘listen‘, () => {
  console.log(`Server running on port ${PORT}`);
});

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

This sets up a basic Oak application and starts the server listening on port 8000.

Note that we‘re using ES modules to import dependencies from URL. Deno does not use npm or a package.json. Instead, it downloads and caches modules on first run.

To execute the server, run:

deno run --allow-net server.ts

The --allow-net flag is required to grant network access permission. By default, Deno has no file, network, or environment access for security.

If you navigate to http://localhost:8000 in the browser, you‘ll see "Not Found". This is expected, as we haven‘t defined any routes yet. Let‘s do that next.

Defining the API Routes

Create a new routes directory with a todo.routes.ts file:

import { Router } from ‘https://deno.land/x/oak/mod.ts‘;

const router = new Router();

router
  .get(‘/todos‘, getTodos)
  .post(‘/todos‘, createTodo)  
  .get(‘/todos/:id‘, getTodoById)
  .put(‘/todos/:id‘, updateTodo)
  .delete(‘/todos/:id‘, deleteTodo);

export default router;

This defines the routes for our Todo API with paths for retrieving all todos, creating a new todo, and getting, updating, and deleting a single todo by ID.

The route handlers like getTodos and createTodo will be implemented in the next step.

Update server.ts to use the routes:

import { Application } from ‘https://deno.land/x/oak/mod.ts‘;
import todoRouter from ‘./routes/todo.routes.ts‘;

const app = new Application();

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

// Listen on port...

Implementing the Controllers

Next, let‘s add the controller functions to handle the logic for each route. Create a controllers directory with a todo.controllers.ts file.

Here‘s an example implementation of getTodos:

import { v4 } from ‘https://deno.land/std/uuid/mod.ts‘;
import todos from ‘../stubs/todos.ts‘;

export const getTodos = ({ response }: { response: any }) => {
  response.body = todos;
};

And here‘s createTodo:

export const createTodo = async (
  { request, response }: { request: any, response: any },
) => {
  const body = await request.body();

  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: ‘No data provided‘ };
    return;
  }

  const { title } = await body.value;

  if (!title) {
    response.status = 422;
    response.body = { msg: ‘Incorrect data. Title is required‘ };
    return;
  }

  const newTodo = {
    id: v4.generate(),
    title,
    isCompleted: false,
  };

  todos.push(newTodo);

  response.body = { msg: ‘Todo created‘, todo: newTodo };
};

Here we‘re extracting the title from the request body. If no title is provided, we return a 422 "Unprocessable Entity" status.

Otherwise, we generate a new ID using the uuid module, set isCompleted to false, and add the todo to our in-memory data store.

The remaining controller functions are similar. They validate the request, interact with the data store, and return an appropriate response.

Remember to update the routes file to import the controller functions:

import {
  getTodos,
  createTodo,
  getTodoById,  
  updateTodo,
  deleteTodo,
} from ‘../controllers/todo.controllers.ts‘;

Using TypeScript Interfaces

To get the full benefit of TypeScript, let‘s define an interface for the shape of a Todo item. Create an interfaces directory with a Todo.ts file:

export default interface Todo {  
  id: string,
  title: string,
  isCompleted: boolean,
}

Now we can use this interface to type-check the data in our controllers:

import Todo from ‘../interfaces/Todo.ts‘;

const newTodo: Todo = {
  id: v4.generate(),  
  title,
  isCompleted: false,  
};

The TypeScript compiler will alert us if we try to assign an incompatible value.

Mocking the Data Store

For simplicity, we‘ll use a mock in-memory array as our data store. In a real application, you‘d use a proper database.

Create a stubs directory with a todos.ts file:

import { v4 } from ‘https://deno.land/std/uuid/mod.ts‘;  

let todos = [
  {
    id: v4.generate(),
    title: ‘Walk the dog‘,
    isCompleted: true,
  },
  {
    id: v4.generate(), 
    title: ‘Buy groceries‘,
    isCompleted: false,
  },
];

export default todos;

We‘re using the uuid module again to generate IDs for the initial todos.

Import this file in the controllers to access the todos array:

import todos from ‘../stubs/todos.ts‘; 

Testing the API

Let‘s test our API using a tool like Postman or curl. Make sure the server is running:

deno run --allow-net server.ts

To get all todos:

curl http://localhost:8000/todos

To create a new todo:

curl -X POST -H "Content-Type: application/json" \
  -d ‘{"title":"New todo"}‘ \
  http://localhost:8000/todos

You can also retrieve, update, and delete individual todos by specifying the ID in the URL path.

Adding Middleware

Middleware allows us to intercept and modify requests and responses. Let‘s add two useful middleware functions: request logging and 404 handling.

Create a middleware directory with a logger.ts file:

import {
  green,
  cyan,
  white,
  bgRed,  
} from ‘https://deno.land/std/fmt/colors.ts‘;

const X_RESPONSE_TIME = ‘X-Response-Time‘;

export default {
  logger: async ({ response, request }, next) => {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(
      `${green(request.method)} ${cyan(request.url.pathname)}`, 
      bgRed(white(responseTime)),
    );
  },
  responseTime: async ({ response }, next) => {  
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    response.headers.set(X_RESPONSE_TIME, `${ms}ms`);
  },
};

The responseTime middleware calculates how long the request takes to process and appends it to the response headers. The logger then logs the request method, path, and response time when the response is returned.

Next, create a notFound.ts file:

export default ({ response }) => {  
  response.status = 404;
  response.body = {
    success: false,
    msg: ‘Not found‘,  
  };
};

This returns a 404 status and error message for any unhandled routes.

Finally, update server.ts to use the middleware:

import { Application } from ‘https://deno.land/x/oak/mod.ts‘;  
import todoRouter from ‘./routes/todo.routes.ts‘;
import logger from ‘./middleware/logger.ts‘;
import notFound from ‘./middleware/notFound.ts‘;

const app = new Application();

app.use(logger.logger);
app.use(logger.responseTime);  
app.use(todoRouter.routes());  
app.use(todoRouter.allowedMethods());
app.use(notFound);

// Listen on port...

Middleware is executed in the order it‘s defined, so make sure to place notFound last to catch any unhandled routes.

Conclusion

Congratulations! You‘ve just built a working Todo API with Deno and Oak. We covered a lot of ground including:

  • Setting up a Deno project and importing dependencies
  • Defining routes for CRUD operations
  • Implementing controllers to handle business logic
  • Using TypeScript interfaces for type safety
  • Testing the API with curl and Postman
  • Adding logging and error handling middleware

Deno‘s simplicity and security focus, combined with familiar-looking frameworks like Oak, make it a compelling choice for JavaScript and TypeScript developers.

There‘s much more we could add to our API, such as:

  • Connecting to a real database
  • Handling authentication and authorization
  • Writing unit and integration tests
  • Adding request validation and sanitization

I encourage you to extend this example and continue exploring Deno. The official Deno Manual and Standard Library are great resources to go deeper.

Thanks for reading, and happy coding!

Similar Posts