• How to Create a Todo API in Deno and Oak
  • By Adeel Imran
  • Originally published: 2020-05-29
  • Translator: hylerrix
  • Note: This article follows the freeCodeCamp translation specification and will be included in the translation of Deno’s Study.
  • Note: the official website of “Deno delve into the art” has been launched! deno-tutorial.js.org

The preface

I’m a JavaScript/Node developer who quietly loves, and even loves, Deno. I was fascinated by Deno from the very beginning, and I’ve been a huge fan ever since, looking forward to playing it.

This article focuses on creating a Todo application based on REST API design. Keep in mind that database operations are not covered in this article and will be covered in more detail in another article.

If you’d like to review or refer to the code in this article at any time, you can visit my repository @adeelibr/deno-playground, which contains all the code in this series.

Another article, How to Use MySQL With Deno and Oak, will be translated soon, and a Demo will be included in The Deno Study.

Photo by Bernard de Clerk/Unsplash

This article will cover

  • Create a basic server
  • Create 5 APIs (Route Routes/Controller)
  • Create a middleware to add terminal output logging to API requests
  • Create a 404 middleware to handle the situation when a user accesses an unknown API

Preparation of knowledge required for this article

  • An already installed Deno environment (don’t worry, I’ll show you how)
  • Superficial knowledge of TypeScript
  • If you have some previous knowledge of Node/Express, it’s better (if you don’t, this article is easy to follow).

Let’s get started

First we need to install Deno. Since I’m using the Mac operating system, I’ll use BREW here. Simply open the terminal and type this command:

$ brew install deno
Copy the code

But if you’re using another operating system, here’s an installation guide: deno. Land Installation. There are a variety of installation options for you to choose from depending on the operating system.

Once you have successfully installed, close the terminal and open the other, type this command:

$ deno --version
Copy the code

If all is well, the terminal will produce the following output:

Using the deno –version command, you can view the version of the installed denO.

Great! With this introduction we have successfully completed 10% of the challenges in this article.

Let’s keep exploring and create a back-end API for our to-do list application.

Preparation of the project

Read on to see all the code in the repository in advance: @adeelibr/deno-playground.

Here we start from scratch:

  • Create a name calledchapter_1:oak(you can name it whatever you want).
  • Use it when you’ve created itcdCommand to enter the folder. Create a name calledserver.tsAnd fill in the following code:
import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const port: number = 8080;

console.log('running on port ', port);
await app.listen({ port });
Copy the code

Let’s run this file first. Open your terminal, go to the root directory of your current project, and type the following command:

$ deno run --allow-net server.ts
Copy the code

Don’t worry, I’ll talk about what the allow-net parameter does later 😄.

If nothing else, this is what you get:

So far, we have created a server application that listens to port 8080. The application can run properly only when port 8080 is not occupied.

If you have any experience with JavaScript development, you may notice that there is a difference in the way we import modules. Here’s how we import modules:

import { Application } from "https://deno.land/x/oak/mod.ts";
Copy the code

When you run the deno run –allow-net

command in the terminal, deno will read your import information and install the modules if they are not installed in the local global environment.

The first execution Deno will try to visit https://deno.land/x/oak/mod.ts module library and install oak. Oak is a Deno Web framework that focuses on writing apis.

The next line we write is:

const app = new Application();
Copy the code

This statement creates an instance of our application, which is the cornerstone of this article’s in-depth exploration of Deno. You can add routes to this instance, configure middleware (such as logging middleware), write a 404 unknown route handler, and so on.

Here’s what we wrote:

const port: number = 8080;
// const port = 8080; // => Can also be written like this
Copy the code

The two lines above are functionally equivalent. The only difference is that const port: number = 8080 tells TypeScript: port that the type of the variable is numeric.

If you write const port: number = “8080”, the terminal will generate an error like this: the port variable should be of type number, but this class is trying to assign it to a string of type “8080”.

If you want to learn more about Types, check out this simple TypeScript official – Base Types documentation. It takes only 2-3 minutes to get back to this article.

At the end of the document we wrote:

console.log('running on port ', port);
await app.listen({ port });
Copy the code

As above, we have Deno listening on port 8080, the port number is dead.

Add more code like this to your server.ts file:

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

const app = new Application();
const port: number = 8080;

const router = new Router();
router.get("/".({ response }: { response: any }) = > {
  response.body = {
    message: "hello world"}; }); app.use(router.routes()); app.use(router.allowedMethods());console.log('running on port ', port);
await app.listen({ port });
Copy the code

What’s new is that both the Application and Router variables are imported from oak.

The code related to the Router is:

const router = new Router();
router.get("/", ({ response }: { response: any }) => {
  response.body = {
    message: "hello world"}; }); app.use(router.routes()); app.use(router.allowedMethods());Copy the code

We create a new router example with the const Router = new Router() statement, and then we create an execution method for handling GET requests for its root directory /.

Let’s focus on the following:

router.get("/".({ response }: { response: any }) = > {
  response.body = {
    message: "hello world"}; });Copy the code

The router-get function takes two arguments. The first argument is the path/to which the route is mounted, and the second argument is a function. The function itself also takes an object argument, which is deconstructed using ES6 syntax, taking only the value of the response variable in it.

Next, write const port: number = 8080 as before; Statement declares the type for the response variable. The {response}: {response: any} statement tells TypeScript that the response variable we’re deconstructing here is of type any.

The any type can help you avoid TypeScript’s strict type checking, and you can learn more about it here.

The next thing I write is to use the response variable and set Response.body. message = “Hello world”; .

response.body = {
  message: "hello world"};Copy the code

Last but not least, we wrote the following two lines of code:

app.use(router.routes());
app.use(router.allowedMethods());
Copy the code

The first line tells Deno to include all paths set in our router variable (currently we only set the root path), and the second line tells Deno to allow any access method to request the path we set, such as GET, POST, PUT, DELETE.

At this point we’re ready to test run ✅. Let’s run this line and see what happens:

$ deno run --allow-net server.ts
Copy the code

— The allow-net parameter tells Deno that the user has granted the application access to the network on an open port.

Now go to http://localhost:8080 in your usual browser and get the following:

We’re almost done with the hard part, but we’re only about 60% of the way to understanding more about the concept.

Great.

The last thing we need to do before we officially start writing the TO-DO list API is to add the following code:

console.log('running on port ', port);
await app.listen({ port });
Copy the code

Replace it with this:

app.addEventListener("listen".({ secure, hostname, port }) = > {
  const protocol = secure ? "https://" : "http://";
  const url = ${protocol}${hostname ?? "localhost"}:${port};
  console.log(Listening on: ${port});
});

await app.listen({ port });
Copy the code

Our previous code simply printed a success log on the console and then asked the application to start listening on the port, which was not very elegant.

In the replacement version, we add an event listener to the application instance using the app.addeventListener (“listen”, ({secure, hostname, port}) => {} statement, and then let the application listen on the port.

The first argument to the listener is the event we want to listen for. Here, listen is the listen event 😅. The second argument is an object that can be deconstructed. In this case, the variables {secure, hostname, port} are deconstructed. The Secure variable is a Boolean type, the hostname variable is a string type, and the port variable is a numeric type.

If you run this application, the log will be generated only if the listener successfully listens on the specified port.

We can take another step further and make it more colorful. Let’s add a new module like this at the top of the server.ts file:

import { green, yellow } from "https://deno.land/[email protected]/fmt/colors.ts";
Copy the code

We can then use the following code in the previous event listener function:

console.log(Listening on: ${port});
Copy the code

Replace with:

console.log(${yellow("Listening on:")} ${green(url)});
Copy the code

Next when we execute:

$ deno run --allow-net server.ts
Copy the code

The following logs are displayed:

It’s so cool that we now have a colorful console.

If you get stuck somewhere, you can go directly to the source repository for this tutorial: @adeelibr/deno-playground.

Let’s create the to-do list API.

  • Create one at the root of your projectroutesFolder, and then create one inside the foldertodo.tsFile.
  • At the same time, create one at the project rootcontrollersFolder, and create one in the foldertodo.tsFile.

Let’s start by filling in the controllers/todo.ts file:

export default {
  getAllTodos: (a)= > {},
  createTodo: async () => {},
  getTodoById: (a)= > {},
  updateTodoById: async () => {},
  deleteTodoById: (a)= >{}};Copy the code

Here we simply export an object that contains many named functions, all of which are currently empty.

Next populate these in the routes/todo.ts file:

import { Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
// controller Controller
import todoController from ".. /controllers/todo.ts";
router
  .get("/todos", todoController.getAllTodos)
  .post("/todos", todoController.createTodo)
  .get("/todos/:id", todoController.getTodoById)
  .put("/todos/:id", todoController.updateTodoById)
  .delete("/todos/:id", todoController.deleteTodoById);

export default router;
Copy the code

The code style above should be familiar to anyone who has written Node and Express.

This includes importing the Route variable from oak and passing const router = new router (); Statement instantiates the reality.

Next we import our controller:

import todoController from ".. /controllers/todo.ts";
Copy the code

The important thing to note here is that in Deno every time we import a local file into a project, we have to complete the file suffix. Otherwise, Deno does not know whether the user wants to import files ending in.js or.ts.

Next we configured all the RESTful style paths we needed for our application with the following code.

router
  .get("/todos", todoController.getAllTodos)
  .post("/todos", todoController.createTodo)
  .get("/todos/:id", todoController.getTodoById)
  .put("/todos/:id", todoController.updateTodoById)
  .delete("/todos/:id", todoController.deleteTodoById);
Copy the code

The above code will resolve the path like this:

Request way API routing
GET /todos
GET /todos/:id
POST /todos
PUT /todos/:id
DELETE /todos/:id

Finally, we export default Router; Statement to export the configured route.

At this point we are done creating the routes (but since our controller is still empty, each route will not do anything, we will add functionality to it).

One final hurdle before we start adding functionality to each controller is that we need to mount this router to our app instance.

So back in the server.ts file we do this:

  • Add this line of code to the top of the file:
/ / routes routing
import todoRouter from "./routes/todo.ts";
Copy the code
  • Delete this section of code:
const router = new Router();
router.get("/".({ response }: { response: any }) = > {
  response.body = {
    message: "hello world"}; }); app.use(router.routes()); app.use(router.allowedMethods());Copy the code
  • Replace it with:
app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
Copy the code

Finally, your server.ts should now look like this:

import { Application } from "https://deno.land/x/oak/mod.ts";
import { green, yellow } from "https://deno.land/[email protected]/fmt/colors.ts";

// routes
import todoRouter from "./routes/todo.ts";

const app = new Application();
const port: number = 8080;

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

app.addEventListener("listen".({ secure, hostname, port }) = > {
  const protocol = secure ? "https://" : "http://";
  const url = `${protocol}${hostname ?? "localhost"}:${port}`;
  console.log(
    `${yellow("Listening on:")} ${green(url)}`,); });await app.listen({ port });
Copy the code

If you get stuck somewhere, you can go directly to the source repository for this tutorial: @adeelibr/deno-playground.

Since there is no functionality on the routing controller, let’s manually add functionality to our controller.

To do this we need to create two (small) files:

  • Create one at the root of your projectinterfacesFolder and create one in itTodo.tsMake sure Todo begins with a capital letter, because if you don’t, it won’t give you any syntax errors here — it’s just a convention.
  • Create one at the project rootstubsFolder and create one in ittodos.tsFile.

Write the following interface description in the file interfaces/ todo.ts:

export default interface Todo {
  id: string,
  todo: string,
  isCompleted: boolean,
}
Copy the code

What is an interface?

One of TypeScript’s core functions is to check the type of a variable. As with const port: number = 8080 and {response}: {response: any} above, we can also detect whether a variable is of object type.

In TypeScript, interfaces are responsible for naming types and are an effective way to define type constraints in and out of code.

Here’s an example of an interface:

// Write an interface
interface LabeledValue {
  label: string;
}

// The labeledObj parameter of this function is the LabeledValue interface type
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = {label: "Size 10 Object"};
printLabel(myObj);
Copy the code

Hopefully, the example above has given you a little more insight into Interface. For more information, check out the official: Interfaces documentation.

Now that we’ve covered enough about interfaces, let’s simulate some fake data (since this article doesn’t deal with databank-related operations).

Let’s populate the todos variable with some analog data in stubs/todos.ts. This will do:

import { v4 } from "https://deno.land/std/uuid/mod.ts";
// interface
import Todo from '.. /interfaces/Todo.ts';

let todos: Todo[] = [
  {
    id: v4.generate(),
    todo: 'walk dog',
    isCompleted: true,
  },
  {
    id: v4.generate(),
    todo: 'eat food',
    isCompleted: false,},];export default todos;
Copy the code
  • There are two things to note: We are referencing a new module here and passing itimport { v4 } from "https://deno.land/std/uuid/mod.ts";The statement deconstructs thev4The variable. And then we use it every timev4.generate()Statements generate a random ID string. thisidCan’t benumberType and need to bestringType of, because we had beforeTodoThe interface is already declaredidThe type of must be string.
  • Another thing to notelet todos: Todo[] = []Statements. This statement tells Deno that our todos variable is aTodoArray (at this point the compiler will know that each element of the array is{id: _string_, todo: _string_ & isCompleted: _boolean_}Type, no other type is allowed).

For more information, check out the official: Interfaces documentation.

Great, you’ve made it this far. Keep up the good work.

Dwayne “the Rock” Johnson appreciates everything you’ve done.

Let’s focus on the controller

In your controllers/todo.ts file:

export default {
  getAllTodos: (a)= > {},
  createTodo: async () => {},
  getTodoById: (a)= > {},
  updateTodoById: async () => {},
  deleteTodoById: (a)= >{}};Copy the code

Let’s write the getAllTodos controller first:

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

export default {
  /** * @description GET todos * @route GET /todos */
  getAllTodos: ({ response }: { response: any }) = > {
    response.status = 200;
    response.body = {
      success: true,
      data: todos,
    };
  },
  createTodo: async () => {},
  getTodoById: (a)= > {},
  updateTodoById: async () => {},
  deleteTodoById: (a)= >{}};Copy the code

Before I get into this code, let me explain the parameter that every controller has — the context parameter.

GetAllTodos: (context) => {} :

getAllTodos: ({ request, response, params }) = > {}
Copy the code

And since we use typescript, we need to add a type declaration for each of these variables:

getAllTodos: (
  { request, response, params }: { 
    request: any, 
    response: any, 
    params: { id: string},},) => {}Copy the code

At this point we add the type description for the three variables {request, Response, params} that we deconstruct.

  • requestThe variables are about the request sent by the user (such as the request header and the JSON class’s request body).
  • responseVariable Information about what the server side returns through the API.
  • paramsVariables are parameters that we define in the route configuration, as follows:
.get("/todos/:id".({ params}: { params: { id: string}}) = > {})
Copy the code

The :id in /todos/:id is a variable used to get dynamic data from the URL. So when the user accesses the API (such as /todos/756), 756 is the value of the: ID parameter. And we know that the value in the URL is of type String.

Now that we have the basic declaration, let’s go back to our Todos controller:

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

export default {
  /** * @description GET todos * @route GET /todos */
  getAllTodos: ({ response }: { response: any }) = > {
    response.status = 200;
    response.body = {
      success: true,
      data: todos,
    };
  },
  createTodo: async () => {},
  getTodoById: (a)= > {},
  updateTodoById: async () => {},
  deleteTodoById: (a)= >{}};Copy the code

For the getAllTodos method we simply need to return the result. If you remember what we said earlier, you’ll remember that response is the data that the server wants to return to the user.

For those of you who have written Node and Express, one big difference here is that we don’t need a Return response object. Deno will automatically perform this operation for us.

The first thing we need to do is to set the response code of this request to 200 through response.status.

More HTTP response codes can be found in the HTTP response status code document on the MDN.

Another thing is to set the value of Response. body to:

{
  success: true,
  data: todos
}
Copy the code

Rerun our server:

$ deno run --allow-net server.ts
Copy the code

Revision: –allow-net attribute tells Deno that this application grants the user access to the network through an open port.

Once your server example runs through, you can request the API in GET/toDOS mode. Here I’m using postman, an add-on for Google Chrome, which can be downloaded here.

You can use any REST style client, but I like using Postman because it’s really simple and easy to use.

In Postman, open a new TAB. Set the request method to GET request and enter http://localhost:8080/todos in the URL input box. Click the Send button to get the desired result:

The GET/toDOS API returns the result.

Cool! One API down, four more waiting for us 👍👍.

If you’re stuck somewhere, look in the accompanying source repository for answers.

Let’s focus on the next controller:

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

export default {
  getAllTodos: (a)= > {},
  /** * @description Add a new todo * @route POST /todos */
  createTodo: async (
    { request, response }: { request: any; response: any= > {},)const body = await request.body();
    if(! request.hasBody) { response.status =400;
      response.body = {
        success: false,
        message: "No data provided"};return;
    }

    // If the request passes, all new Todos are returned
    let newTodo: Todo = {
      id: v4.generate(),
      todo: body.value.todo,
      isCompleted: false};let data = [...todos, newTodo];
    response.body = {
      success: true,
      data,
    };
  },
  getTodoById: (a)= > {},
  updateTodoById: async () => {},
  deleteTodoById: (a)= >{}};Copy the code

Since we are going to add a new Todo to the list, I imported two generic modules into the controller file:

  • import { v4 } from "https://deno.land/std/uuid/mod.ts"Statement to create a unique identity for each TODO element.
  • import Todo from ".. /interfaces/Todo.ts";Statement to ensure that the newly created ToDO complies with the interface format standard of the ToDO element.

Our createTodo controller is an async representative of async and we’re going to use some Promise techniques in our functions.

Let’s cut off a little bit of the explanation:

const body = await request.body();
if(! request.hasBody) { response.status =400;
  response.body = {
    success: false,
    message: "No data provided"};return;
}
Copy the code

First we read the JSON content from the user in the request body. Next we use Oak’s built-in Request. hasBody method to check whether the user sent the content is empty. If it’s empty, we’ll go to if (! Request.hasbody) {} code block.

In this case, we set the status code of the response body to 400 (400 indicates something wrong with the client), and the response body returned by the server to {success: false, message: “no data provided}. Then the program simply executes return; Statement to ensure that the following code will not be executed.

Let’s write it like this:

// If the request passes, all new Todos are returned
let newTodo: Todo = {
  id: v4.generate(),
  todo: body.value.todo,
  isCompleted: false};let data = [...todos, newTodo];
response.body = {
  success: true,
  data,
};
Copy the code

We create a new Todo element with the following code:

let newTodo: Todo = {
  id: v4.generate(),
  todo: body.value.todo,
  isCompleted: false};Copy the code

Let newTodo: Todo = {} to ensure that the value of the newTodo variable follows the same interface format as any other Todo element. We then assign a random ID using v4.generate(), set the key value of todo to body.value.todo and the value of the isCompleted variable to false.

What you need to know here is that the content sent by the user to us can be obtained through the body. Value in oak.

Here’s what we do:

let data = [...todos, newTodo];
response.body = {
  success: true,
  data,
};
Copy the code

Here newTodo is added to the overall toDo list and {success: true & data: data is returned in the body of the response.

This controller also runs successfully ✅.

Let’s rerun our server:

$ deno run --allow-net server.ts
Copy the code

In Postman, I open a new TAB. Set the request for the POST type, and enter http://localhost:8080/todos in the URL input box, click the Send will get the following results:

Because empty content was sent in the request body above, you get the 400 error response code and the reason for the error.

But if we add the following JSON content to the request body and resend it:

Using {todo: “eat a lamma”} to show the successful result of a POST /todos, we can see that new elements have been added to the list.

Cool, I can see that our apis are executing in the expected way one by one.

Two apis down, three to go.

We’re almost there, because we’ve covered most of the hard stuff. ☺ ️ 🙂 🤗 🤩

Let’s look at the third API:

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

export default {
  getAllTodos: (a)= > {},
  createTodo: async() = > {},/** * @description Todo * @route GET todos/: ID */
  getTodoById: (
    { params, response }: { params: { id: string }; response: any= > {},)const todo: Todo | undefined = todos.find((t) = > {
      return t.id === params.id;
    });
    if(! todo) { response.status =404;
      response.body = {
        success: false,
        message: "No todo found"};return;
    }

    // If todo finds it
    response.status = 200;
    response.body = {
      success: true,
      data: todo,
    };
  },
  updateTodoById: async () => {},
  deleteTodoById: (a)= >{}};Copy the code

Let’s start with the controller under GET todos/: ID, which looks for the corresponding todo element by ID.

Let’s continue our analysis by taking small snippets:

const todo: Todo | undefined = todos.find((t) = > t.id === params.id);
if(! todo) { response.status =404;
  response.body = {
    success: false,
    message: "No todo found"};return;
}
Copy the code

In the first line we declare a const todo variable and set its type to either todo or undefined. So a ToDO element can only be a variable conforming to the ToDO interface specification or an undefined value, and not anything else.

Todos.find ((t) => t.id === params.id); Statement to find the specified Todo element through the array.find () method and the params.id value. If we find it we get a Todo element of type Todo, otherwise we get undefined.

If the todo value is undefined, it means that the code in the if condition will execute:

if(! todo) { response.status =404;
  response.body = {
    success: false,
    message: "No todo found"};return;
}
Copy the code

Here we set the response status code to 404, which means not found, and return the standard {status, message} body.

Isn’t that cool? 😄

Let’s simply write:

// If todo finds it
response.status = 200;
response.body = {
  success: true,
  data: todo,
};
Copy the code

Set a response body with a status code of 200 and return success: true & data: Todo contents.

Let’s test this in Postman:

Restart the server together:

$ deno run --allow-net server.ts
Copy the code

In the postman, continue to open a new TAB, set the request method to GET request and enter http://localhost:8080/todos/:id in the URL input box, click on the Send to execute the request.

Since we used the random ID generator, first we need to call the API that gets all the elements. Select an ID from the list of elements to test the new API. Every time you restart Deno, a new ID is generated.

We type this:

The server returns a 404 and tells us that no data was found.

But if you enter a correct ID, the server will return the same ID as that ID and the response status is 200.

If you need to refer to the source code for this article, you can visit it here: @adeelibr/deno-playground.

Good. Three apis down. Two to go.

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

export default {
  getAllTodos: (a)= > {},
  createTodo: async () => {},
  getTodoById: (a)= > {},
  /** * @description Update todo by id * @route PUT todos/:id */
  updateTodoById: async (
    { params, request, response }: {
      params: { id: string },
      request: any,
      response: any,},) => {const todo: Todo | undefined = todos.find((t) = > t.id === params.id);
    if(! todo) { response.status =404;
      response.body = {
        success: false,
        message: "No todo found"};return;
    }

    // Update the corresponding todo if it is found
    const body = await request.body();
    constupdatedData: { todo? :string; isCompleted? :boolean } = body.value;
    let newTodos = todos.map((t) = > {
      returnt.id === params.id ? {... t, ... updatedData } : t; }); response.status =200;
    response.body = {
      success: true,
      data: newTodos,
    };
  },
  deleteTodoById: (a)= >{}};Copy the code

Let’s look at the next controller, PUT toDOS /: ID. This controller updates the contents of an element.

Let’s continue to truncate the code to take a closer look:

const todo: Todo | undefined = todos.find((t) = > t.id === params.id);
if(! todo) { response.status =404;
  response.body = {
    success: false,
    message: "No todo found"};return;
}
Copy the code

And what we’re doing here is the same thing that our previous controllers did, so I’m not going to go into it.

Advanced tip: If you want to make this code a generic block and use it in both controllers, that’s fine.

Here’s what we do:

// Update the corresponding todo if it is found
const body = await request.body();
constupdatedData: { todo? :string; isCompleted? :boolean } = body.value;
let newTodos = todos.map((t) = > {
  returnt.id === params.id ? {... t, ... updatedData } : t; }); response.status =200;
response.body = {
  success: true,
  data: newTodos,
};
Copy the code

The code I want to focus on here is the following:

constupdatedData: { todo? :string; isCompleted? :boolean } = body.value;
let newTodos = todos.map((t) = > {
  returnt.id === params.id ? {... t, ... updatedData } : t; });Copy the code

First, we execute const updatedData = body.value, and then add type checking to updatedData as follows:

updatedData: { todo? :string; isCompleted? :boolean }
Copy the code

This little piece of code tells TS that the updatedData variable is a familiar object that may or may not contain toDO and isComplete.

Next we iterate over each todo element, like this:

let newTodos = todos.map((t) = > {
  returnt.id === params.id ? {... t, ... updatedData } : t; });Copy the code

Where params.id and t.id are the same, we override the contents of the object with the contents that the user passed to us and wants to change to.

We also wrote the API successfully.

Let’s restart the server:

$ deno run --allow-net server.ts
Copy the code

Open a TAB in Postman. Sets the request method to PUT, and enter http://localhost:8080/todos/:id in the URL input box, click the Send:

Since we used the random ID generator, first we need to call the API that gets all the elements. Select an ID from the list of elements to test the new API.

Each time the Deno program is restarted, a new ID is generated.

The 404 status code is returned and tells us that the associated Todo element was not found.

Provide a known ID and fill in the body of the request with what needs to be changed. The server returns a changed element and all other elements.

Cool, four apis down and we only have one more to do.

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

export default {
  getAllTodos: (a)= > {},
  createTodo: async () => {},
  getTodoById: (a)= > {},
  updateTodoById: async() = > {},/** * @description Todo * @route DELETE todos/: ID */
  deleteTodoById: (
    { params, response }: { params: { id: string }; response: any= > {},)const allTodos = todos.filter((t) = >t.id ! == params.id);// remove the todo w.r.t id and return
    // remaining todos
    response.status = 200;
    response.body = {
      success: true, data: allTodos, }; }};Copy the code

Let’s finally discuss the execution of the Delete todos/: ID controller, which removes the corresponding TOdo element with the given ID.

We simply add a filter here:

const allTodos = todos.filter((t) => t.id ! == params.id);Copy the code

Iterates through all the elements and removes the elements with the same todo.id and params.id values, and returns all the remaining elements.

Let’s write it like this:

// Delete the todo and return everything else
response.status = 200;
response.body = {
  success: true,
  data: allTodos,
};
Copy the code

Simply return a list of all to-do items that do not have the same todo.id.

Let’s restart the server:

$ deno run --allow-net server.ts
Copy the code

Open a TAB in Postman. Sets the request method to PUT, and enter http://localhost:8080/todos/:id in the URL input box, click the Send:

Since we used the random ID generator, first we need to call the API that gets all the elements. Select an ID from the list of elements to test the new API. Every time you restart Deno, a new ID is generated.

Each time the Deno program is restarted, a new ID is generated.

We finally nailed all five apis.

Now we only have two things left:

  • Add a 404 middleware to allow users to be alerted when accessing a non-existent route.
  • Add a logging API to print the execution time of all requests.

Create a 404 routing middleware

Create a folder named middlewares in the root of your project, and after creating a file called notfound.ts in it, add the following code:

export default ({ response }: { response: any }) => {
  response.status = 404;
  response.body = {
    success: false,
    message: "404 - Not found."}; };Copy the code

The code above doesn’t introduce anything new – it uses a familiar style to our controller structure. This simply returns a 404 status code (indicating that the relevant route was not found) and a piece of JSON content: {success, message}.

Next, add the following to your server.ts file:

  • Add the relevant import statement at the top of the file:
// Not found
import notFound from './middlewares/notFound.ts';
Copy the code
  • The next inapp.use(todoRouter.allowedMethods())Add the following:
app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());

// 404 page
app.use(notFound);
Copy the code

Execution order is important here: whenever we try to access an API route, it matches/checks the route from todoRouter first. If notFound, it executes app.use(notFound); Statements.

Let’s see if it works.

Restart the server:

$ deno run --allow-net server.ts
Copy the code

Open a TAB in Postman. Sets the request method to PUT, and enter http://localhost:8080/todos/:id in the URL input box, click the Send:

So we now have a routing middleware that will app.use(notFound); After the other routes in the server.ts file. If the request route does not exist, it executes and returns a 404 status code (meaning not found) and simply returns a response message, {success, message}, as usual.

Advanced tip: We’ve constrained {success, message} to be the format returned if the request fails, and {success, data} to be the format returned to the user if the request succeeds. Therefore, we can even treat it as an object interface and add it to the project to ensure interface consistency and secure type checking.

Cool, now that we’ve got one of the middleware out of the way — let’s add another middleware to print logs at the terminal.

Keep in mind: if you get stuck in some places, check out the source code for this article: @adeelibr/deno-playground.

Middleware that prints logs on terminals

Create a new logger.ts file in your middlewares folder and fill it with the following:

import {
  green,
  cyan,
  white,
  bgRed,
} from "https://deno.land/[email protected]/fmt/colors.ts";

const X_RESPONSE_TIME: string = "X-Response-Time";

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

Add the following code to the server.ts file:

  • Import modules by adding an import statement to the top of the file:
// logger
import logger from './middlewares/logger.ts';
Copy the code
  • Mentioned earliertodoRouterAdd middleware code in front of the code like this:
// The order in which the following code is written is important
app.use(logger.logger);
app.use(logger.responseTime);

app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
Copy the code

Now let’s talk about what happened.

Let’s discuss logger.ts first, and cut it off here:

import {
  green,
  cyan,
  white,
  bgRed,
} from "https://deno.land/[email protected]/fmt/colors.ts";
Copy the code

Here I have imported modules about terminal colors that I want to use in our logging middleware.

This is similar to how we used eventListener in the previous server.ts file. We will use colored log information to log our API requests.

Const X_RESPONSE_TIME: string = “x-response-time “; . This statement is used to inject the value of the X_RESPONSE_TIME variable: x-response-time into the Response Header when the user request arrives. I’ll explain that later.

Then we export an object like this:

export default {
  logger: async ({ response, request }, next) {}
  responseTime: async ({ response }, next) {}
};
Copy the code

At this point we use this in server.ts:

// The order in which the following two lines are written is important
app.use(logger.logger);
app.use(logger.responseTime);
Copy the code

Now let’s discuss what the logging middleware actually does, and illustrate its implementation with next().

The figure above shows the execution order of the logging middleware when the GET/ToDOS API is called.

The only difference between this controller and the previous one is the use of the next() function, which helps us jump from one controller to another, as shown in the figure above.

Hence this paragraph:

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

Notice how we wrote it in server.ts:

// The order in which the following code is written is important
app.use(logger.logger);
app.use(logger.responseTime);

app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
Copy the code

The execution order is as follows:

  • Logger. Logger middleware
  • Logger. ResponseTime middleware
  • The todoRouter controller (whatever route the user wants to access, for interpretation purposes, it’s assumed that the user callsGET /todosTo get all the todo items).

So the contents of logger.logger will be executed first:

logger: async (
    { response, request }: { response: any, request: any },
    next: Function,
  ) => {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(${green(request.method)} ${cyan(request.url.pathname)});
    console.log(${bgRed(white(String(responseTime)))});
  },
Copy the code

Upon encountering await next(), it immediately jumps to the next middleware responseTime.

Share this image again to review the process.

In responseTime, only the following two lines are executed first (see Execution procedure 2 above) :

const start = Date.now();
await next();
Copy the code

Then jump to the getAllTodos controller and execute all the code in getAllTodos.

We don’t need to use next() in this controller, it will automatically return to the responseTime middleware and do the following:

const ms: number = Date.now() - start;
response.headers.set(X_RESPONSE_TIME, ${ms}ms)
Copy the code

Now we know the sequence of execution of 2, 3, and 4 (see figure above).

Here’s what happens:

  • And we do that by executingconst start = Date.now();To capture themsIs a unit of data. Then, we immediately callnext()To jump togetAllTodosController and run the code in it. And then we go back toresponseTimeIn the controller.
  • Then, through executionconst ms: number = Date.now() - start;To subtract the time the request first came in. In this case, it will return a number of milliseconds off, which will tell Deno all the time it took to execute the getAllTodos controller.

Share this document again to review the process:

  • Next we’re going toresponseSet the following parameters in the response header:
response.headers.set(X_RESPONSE_TIME, ${ms}ms)
Copy the code

Set the value of x-Response-time to the number of milliseconds taken by the Deno getAllTodos API.

  • Then return from execution sequence 4 to execution sequence 5 (see figure above).

Simply write it here:

const responseTime = response.headers.get(X_RESPONSE_TIME);
console.log(${green(request.method)} ${cyan(request.url.pathname)});
console.log(${bgRed(white(String(responseTime)))});
Copy the code
  • We print logs fromX-Response-TimeThe elapsed time to execute the API is obtained in
  • Next we print it in a colored font on the terminal.

Request. method returns the method of the user’s request, such as GET, PUT, etc., while request.url.pathname returns the path of the user’s request, such as /todos.

Let’s see if it works.

Restart the server:

$ deno run --allow-net server.ts
Copy the code

Open a TAB in Postman. Sets the request method to GET, and enter http://localhost:8080/todos in the URL input box, click the Send:

When you request the API a few more times in Postman and go back to the console to look at the log, you should see something like this:

Each API request is logged by the logging middleware at the terminal.

That’s it — we got it done.

If you get stuck somewhere, check out the full source code for this article: github.com/adeelibr/de…

I hope you found this article helpful and that it really helped you learn something new.

If you like it, please share it on social media. If you want to get in touch, you can contact me on Twitter.