loading...
Cover image for Develop and Dockerize a Blogging API With Deno, Oak, and MySQL

Develop and Dockerize a Blogging API With Deno, Oak, and MySQL

fhsinchy profile image Farhan Hasin Chowdhury Updated on ・21 min read

If you're someone who has been working with Node.js for a while, chances are you've heard of Deno, the new JavaScript runtime with special attention towards security.

Although it's at an early stage of its life, there are a good number of modules out there. Thanks to services like Pika, one can even use npm packages inside Deno.

In this project, my goal was to see how much effort it takes to build a simple API with authentication using whatever modules are available for the runtime at this moment and also if I can dockerize it with ease or not.

I must admit, it was easier than I thought. By utilizing some third party modules and an excellent hayd/deno docker image created by Andy Hayden, I was able to get a functional blogging API up and running with stateless authentication within around 4 hours. The modules I've used are:

  • oak - a middleware framework for Deno's net server.
  • deno_mysql - MySQL and MariaDB (5.5 and 10.2+) database driver for Deno.
  • bcrypt - a is a port of jBCrypt to TypeScript for use in Deno.
  • djwt - the absolute minimum to make JSON Web Tokens in Deno.
  • slugify - a string slugifier.

This article assumes that you have a working knowledge of Express or Koa, JavaScript, SQL, and Docker. The entire project is written in TypeScript. Given the code here makes use of only a few TypeScript features, it should be easy to understand.

This isn’t going to be an introduction to Deno either. If you’re looking for that, The Deno Handbook by Flavio Copes is a great resource.


Source Code

Source code for this tutorial can be found in the simplified-tutorial-version branch of following repository: 

GitHub logo fhsinchy / deno-blog

An experimental blogging application developed with Docker, Deno, Oak and MySQL


Introduction to Oak

Oak is a middleware framework for Deno inspired by the popular Koa middleware framework. Before we begin working on the project, having an understanding of a few concepts like middleware and routing is crucial.

Middleware

Oak middleware are functions that execute during the lifecycle of a request to the server. All middleware in Oak has access to a context object. To see a middleware in action, create a file app.ts somewhere in your computer and put following code in it:

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

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

console.log("app running -> http://localhost:3000");
await app.listen({ port: 3000 });

The code can be run by executing following command on your terminal -

deno run --allow-net app.ts

Deno uses URLs for importing modules and third-party modules can be pulled in from https://deno.land/x which is a hosting service for ES modules.

Once the Application class is imported from Oak, an app instance can be created. app.use() function is used for registering middleware. The middelware function is passed as a parameter to the app.use() call. The context object is usually denoted by ctx and contains things like the request and response objects. If you want to learn about the context in more detail follow this link.

A middleware can either end a request by returning a response or can pass it to the next one using the next method. Middleware are processed as stack. A more complex example with middleware for logging incoming requests with response time can be created with following the code:

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

const app = new Application();

// Logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.headers.get("X-Response-Time");
  console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
});

// Timing
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});

// Hello World!
app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

console.log("app running -> http://localhost:3000");
await app.listen({ port: 3000 });

Two new middleware have been added for logging the incoming request and the time took to respond in the console.

The logger middleware just logs the value of X-Response-Time header along with the request method and URL and the timer middleware sets the value of X-Response-Time header used in our previous middleware.

All the three midleware in this program will be stacked on top of one another and executed in the order we register them in the code. At first the logger will run, then the timer middleware and at last the hello world middleware. You can test out the code again by restarting the server.

Middleware can be exported and imported as ES modules. Create a directory called middleware on the root of your project and create two files named logger.ts and timer.ts in there. Now extract the code for logger middleware and put that inside the logger.ts file:

export default async (ctx: any, next: any) => {
  await next();
  const rt = ctx.response.headers.get("X-Response-Time");
  console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
}

Now for the timer.ts:

export default async (ctx: any, next: any) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
}

Now import these two exported functions inside app.ts and register using app.use() function:

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

import logger from "../middleware/logger.ts";
import timer from "../middleware/timer.ts";

const app = new Application();

app.use(logger);
app.use(timer);

// Hello World!
app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

console.log("app running -> http://localhost:3000");
await app.listen({ port: 3000 });

Yeah, that's better. This directory will be now used as our project root. As we go forward more complex middleware will be added.

Routing

In Oak, the Router class can be used for producing middleware to enable routing based on the path-name of the request. So far the application responds with hello world no matter what endpoint we hit, that's not what we want. So, update the code for hello world middleware inside app.ts to look like this:

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

import logger from "../middleware/logger.ts";
import timer from "../middleware/timer.ts";

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

app.use(logger);
app.use(timer);

// Hello World!
router.get("/", (ctx) => {
  ctx.response.status = Status.OK;
  ctx.response.type = "json";
  ctx.response.body = {
    status: "success",
    message: "Hello World!",
    data: null,
  };
});

app.use(router.routes());

console.log("app running -> http://localhost:3000");
await app.listen({ port: 3000 });

To use the Router class, an instance of it has to be created. Routes with GET, POST, PUT, PATCH, DELETE methods can be created by calling the corresponding function on the Router instance. You can learn more about this class by following this link.

Each route registration takes the path-name as a string and a function as middleware. Just like the app.use() call, all route middleware has access to the context.

Status code for the response can be set using ctx.response.status property. A status is a simple number, the Status class provides properties containing status codes for various situations so, Status.OK is 200, Status.NotFound is 404, you get the idea.

Type and contents of the response can be set using ctx.response.type and ctx.response.body properties.

Routes can be registered in the app instance using app.use() call passing router.routes() as a parameter, where router is the instance of Router class.

I'm using JSend - a specification for a simple, no-frills, JSON based format for application-level communication but you're free to use whatever you like.


Project Structure

I will try not to complicate the project structure very much, only what's necessary:

Project Structure

To create this folder structure open your terminal inside your project root and execute the following command:

mkdir controllers db docker-entrypoint-initdb.d routes; touch controllers/blogs.ts controllers/auth.ts db/mysql.ts docker-entrypoint-initdb.d/blogs.sql docker-entrypoint-initdb.d/users.sql middleware/authorize.ts middleware/error.ts routes/blogs.ts routes/auth.ts

We've already created the middleware directory, middleware/logger.ts, middleware/timer.ts and app.ts files so, I'm skipping them in the command.

Once the structure is ready, open the project in a code editor like Visual Studio Code.

An official VSCode extension is available in the market place - Deno

The first thin we'll deal with is the Docker setup in next section.


Docker Setup

Open up the Dockerfile and update its content as follows:

FROM hayd/alpine-deno:latest

EXPOSE 3000

WORKDIR /usr/app

COPY . .

CMD [ "run", "--unstable", "--allow-net", "--allow-env", "--allow-read", "app.ts" ]

I've used the excellent hayd/deno image as the base. The dockerfile loads the base image, sets a working directory, copies all the project file and, sets the default command to run.

Entry-point for this image is the deno executable itself, so all we need to pass in the CMD instruction is the list of arguments to be passed.

If you want to learn more about dockerfile, you can from the official Dockerfile reference page.

Next, open up the docker-compose.yml and update its content as follows:

version: "3.8"

services: 
    db:
        image: mysql:5.7.30 # https://hub.docker.com/_/mysql
        command: --default-authentication-plugin=mysql_native_password --explicit_defaults_for_timestamp
        restart: always
        volumes: 
            - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
        environment:
            MYSQL_DATABASE: "denoblog"
            MYSQL_ROOT_PASSWORD: 63eaQB9wtLqmNBpg
    api:
        build: .
        restart: always
        depends_on: 
            - db
        volumes: 
            - ./:/usr/app
        ports: 
            - 3000:3000
        environment: 
            - DB_HOST=db # this should be identical to the database service name
            - DB_USER=root
            - DB_DATABASE=denoblog
            - DB_PASSWORD=63eaQB9wtLqmNBpg
            - TOKEN_SECRET=QA3GCPvnNO3e6x29dFfzbvIlP8pRNwif # don't forget to change this

In this compose file, there are two services. For the db service, I'm using the official mysql image with a version tag of 5.7.30. All the configuration options used here can be found on the image page at the docker hub.

The api service uses our previously created dockerfile for building the container. The rest of the file is pretty self-explanatory I think.

Again if you want to learn more about docker compose file you from the official Compose file version 3 reference page.

Install the official docker extension of VSCode to avoid unwanted mistakes - Docker

The last thing we have to is write the SQL code for table initialization. Open up docker-entrypoint-initdb.d/blogs.sql and update its content as follows:

CREATE TABLE IF NOT EXISTS blogs (
    id int(11) NOT NULL AUTO_INCREMENT,
    title varchar(255) NOT NULL,
    content text NOT NULL,
    slug varchar(255) NOT NULL UNIQUE,
    created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

And update docker-entrypoint-initdb.d/users.sql file content as follows:

CREATE TABLE IF NOT EXISTS users (
    id int(11) NOT NULL AUTO_INCREMENT,
    name varchar(255) NOT NULL,
    email varchar(255) NOT NULL UNIQUE,
    password varchar(255) NOT NULL,
    created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

The MySQL docker image executes any SQL file inside docker-entrypoint-initdb.d directory in the order they appear. To test out our set-up, open-up terminal inside the project root and run following command:

docker-compose up --build

There will be a wall of text but look for something like the following:

db_1   | 2020-06-08T17:27:05.115386Z 0 [Note] Event Scheduler: Loaded 0 events
db_1   | 2020-06-08T17:27:05.116258Z 0 [Note] mysqld: ready for connections.
db_1   | Version: '5.7.30'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

If everything goes fine the application should be running on http://127.0.0.1:3000 address.

You can stop the application by hitting control + c key combination. The --build parameter is only necessary when you have to rebuild the container. Otherwise you can use docker-compose up to restart the container when needed.

You can learn more about docker-compose command line interface from the official Compose command-line interface reference page.

Also I think this is a good time to configure the database driver. In this project, we won't be using any ORM or query builder. Instead, we'll use the MySQL driver itself to execute our queries. So open up the db/mysql.ts file and add following code to it:

import { Client } from "https://deno.land/x/mysql/mod.ts";

const client = await new Client().connect({
  hostname: Deno.env.get("DB_HOST"),
  username: Deno.env.get("DB_USER"),
  password: Deno.env.get("DB_PASSWORD"),
  db: Deno.env.get("DB_DATABASE"),
});

export default client;

Here, we're importing Client class and creating a connection to the database. The connect() function takes necessary configuration as its parameter. These configuration parameters will come from the environment as set in the docker-compose.yml file. We're then default exporting the object.

Now that we have Docker set-up we're going to work on the API functionalities. And for that we're going to follow these three steps:

  • Write controller function.
  • Import and register controller function as route middleware.
  • Register routes from router instance in app instance.

So let's begin implementing the CRUD functionalities.


CRUD Operations

We'll implement five endpoints:

  • Index - for returning all the blogs in database.
  • Read - for returning single blog based on slug.
  • Create - for creating new blogs.
  • Update - for updating individual blogs.
  • Delete - for deleting individual blogs.

As mentioned previously, we'll implement functionalities in three steps.

Step 1: Open up the controllers/blogs.ts file and put following code in it:

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

import client from "../db/mysql.ts";

export async function index(ctx: any) {
    const blogs: any = (await client.execute("SELECT * FROM blogs")).rows;

    ctx.response.status = Status.OK;
    ctx.response.type = "json";
    ctx.response.body = {
        status: "success",
        message: `${blogs.length} blogs found in database`,
        data: { blogs },
    }
}

Here is the middleware function responsible for getting all blogs from the database. This function will be used as route middleware in the next step.

The code is almost self-explanatory. We're importing MySQL client from db/mysql.ts. Running a SELECT query by calling the client.execute() function. The client.execute() function returns rows inside an array that we're saving inside a variable called blogs. We're then returning a well-formatted response with the appropriate status code, type, and payload.

If you want to learn more about about the usage of the Deno MySQL database driver follow this link.

Step 2: In this step, open up routes/blogs.ts file and update it's content to look like the following:

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

import { index, } from "../controllers/blogs.ts";

const router = new Router();

router.get("/blogs", index);

export default router;

We're importing the index function from controllers/blogs.ts file and using that as the middleware for /blogs route. We're also exporting the router object from this file.

Step 3: The last step is registering the routes in the app instance. Open up app.ts file and update its code to look like the following -

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

import logger from "./middleware/logger.ts";
import timer from "./middleware/timer.ts";

import blogs from "./routes/blogs.ts";

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

app.use(logger);
app.use(timer);

router.get("/", (ctx) => {
  ctx.response.status = Status.OK;
  ctx.response.type = "json";
  ctx.response.body = {
    status: "success",
    message: "Hello World!",
    data: null,
  };
});

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

console.log("app running -> http://localhost:3000");
await app.listen({ port: 3000 });

Most of the code in this file remains unchanged from our previous example. Only new things are the importing of the router object from routes/blogs.ts at line 6 and registration of the routes in the app instance at line 25.

With all the three previously mentioned steps done, now we're ready to test out this endpoint. Restart the server and visit http://127.0.0.1:3000/blogs to see the output it produces:

Blogs Index Route Postman

Assuming that you've done the docker setup properly and have not made any mistakes in your code, you should see a success response. I have previously created blogs in the database, but in your case, you should get 0 blogs and an empty array.


Error Handling

As you may have already noticed there is a surprising lack of error handling in this application. If the code fails at any given time, it's hard to understand what's going on as all we're gonna get is a blank response with a status code.

One thing that we can do is we can put try-catch blocks inside our controller functions so that we can respond properly in cases of failures. But then we'll have to copy and paste the same looking error responses in all our controller functions which are not ideal.

A better idea is to write a global middleware like the ones we've written earlier and do the error handling centrally. So open up middleware/error.ts and put following code in it:

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

export default async (ctx: any, next: any) => {
  try {
    await next();

    const status = ctx.response.status || Status.NotFound;

    if (status === Status.NotFound) {
      ctx.throw(Status.NotFound, "Not Found!");
    }
  } catch (err) {
    if (isHttpError(err)) {
      const status = err.status;

      ctx.response.status = status;
      ctx.response.type = "json";
      ctx.response.body = {
        status: status >= 400 && status < 500 ? "fail" : "error",
        message: err.message,
      };
    }
  }
};

This is a simple middleware that catches any error thrown within the app context. In the try block, we pass control to the next middleware in the stack by calling next(). We also check if the user has hit a non-existent route by checking the value of ctx.response.status, if yes we throw a not found error within the app context, catch that and respond with a nicely formatted error response.

Open up app.ts, import the middleware, and register it along with the other middleware functions.

// previously written codes
import error from "./middleware/error.ts";

// previously written codes
app.use(error);

That's it, the error handling middleware should be ready for action now. Try this out by visiting a non existent endpoint:

Error Handler

That's good enough for this simple project. Lets carry on with the other CRUD endpoints following our three steps procedure.


Step 1: Open up the controllers/blogs.ts file again and add rest of the four functions. The final form of the file should look like as follows:

import { Status } from "https://deno.land/x/oak/mod.ts";
import { slugify } from "https://deno.land/x/slugify/mod.ts";

import client from "../db/mysql.ts";

export async function index(ctx: any) {
  const blogs: any = (await client.execute("SELECT * FROM blogs")).rows;

  ctx.response.status = Status.OK;
  ctx.response.type = "json";
  ctx.response.body = {
    status: "success",
    message: `${blogs.length} blogs found in database`,
    data: { blogs },
  };
}

export async function store(ctx: any) {
  const body = await ctx.request.body();

  const title = body.value.title;
  const slug = slugify(body.value.title, { lower: true });
  const content = body.value.content;

  const result: any = await client.execute(
    "INSERT INTO blogs (title, slug, content) VALUES (?, ?, ?)",
    [title, slug, content],
  );

  ctx.response.status = Status.Created;
  ctx.response.type = "json";
  ctx.response.body = {
    status: "success",
    message: `${result.affectedRows} blog created in database`,
    data: {
      blog: {
        id: result.lastInsertId,
      },
    },
  };
}

export async function show(ctx: any) {
  const result = await client.execute(
    "SELECT * FROM blogs WHERE slug = ?",
    [ctx.params.slug],
  );
  const rows: any = result.rows;

  if (rows.length > 0) {
    const blog = {
      id: rows[0].id,
      title: rows[0].title,
      content: rows[0].content,
      created_at: rows[0].created_at,
    };

    ctx.response.status = Status.OK;
    ctx.response.type = "json";
    ctx.response.body = {
      status: "success",
      message: `Blog with slug ${ctx.params.slug}`,
      data: { blog },
    };
  } else {
    ctx.throw(Status.NotFound);
  }
}

export async function update(ctx: any) {
  const result = await client.execute(
    "SELECT * FROM blogs WHERE slug = ?",
    [ctx.params.slug],
  );
  const rows: any = result.rows;

  if (rows.length > 0) {
    const blog = {
      id: rows[0].id,
      title: rows[0].title,
      content: rows[0].content,
      created_at: rows[0].created_at,
    };

    const body = await ctx.request.body();

    blog.title = body.value["title"] ? body.value["title"] : blog.title;
    blog.content = body.value["content"] ? body.value["content"] : blog.content;

    await client.execute(
      "UPDATE blogs SET title = ?, content = ? WHERE slug = ?",
      [blog.title, blog.content, ctx.params.slug],
    );

    ctx.response.status = Status.OK;
    ctx.response.type = "json";
    ctx.response.body = {
      status: "success",
      message: `Blog with slug ${ctx.params.slug} updated`,
      data: { blog },
    };
  } else {
    ctx.throw(Status.NotFound);
  }
}

export async function destroy(ctx: any) {
  const result = await client.execute(
    "SELECT * FROM blogs WHERE slug = ?",
    [ctx.params.slug],
  );
  const rows: any = result.rows;

  if (rows.length > 0) {
    await client.execute("DELETE FROM blogs WHERE slug = ?", [ctx.params.slug]);

    ctx.response.status = Status.OK;
    ctx.response.type = "json";
    ctx.response.body = {
      status: "success",
      message: `Blog with slug ${ctx.params.slug} deleted`,
      data: null,
    };
  } else {
    ctx.throw(Status.NotFound);
  }
}

Nothing out of ordinary here. Four functions store, show, update, destroy have been added. 

In the store function, we are taking the payload passed by the client and inserting that into the database with an INSERT query. We can get the payload passed by the client by calling the ctx.request.body() method. Slugify is used for generating URL friendly slug from the blog title.

In the show, update and delete functions we're using the slug to fetch a blog post from the database. This slug will be passed as a dynamic path-parameter to this function in the next step. We're also throwing a "not found" exception in cases of non-existent slugs. These exceptions will be caught by our error handler middleware automatically.

In a real-life scenario, I don't put database queries directly in controller methods like that. The master branch code has a better solution for this problem but for now, let's keep things as simple.

Step 2: Open up routes/blogs.ts and add the newly created middleware with corresponding endpoints. The file's content should look like as follows:

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

import { index, show, store, update, destroy } from "../controllers/blogs.ts";

const router = new Router();

router.get("/blogs", index)
      .post("/blogs", store)
      .get("/blogs/:slug", show)
      .put("/blogs/:slug", update)
      .delete("/blogs/:slug", destroy);

export default router;

Routes for creating, updating, and deleting blog entries are going to be of post, put and delete method respectively.

At lines 9, 10, and 11 you can see how to create dynamic path-names. All path parameters are stored in the context inside ctx.params object so the value of :slug can be accessed like this ctx.params.slug inside the controller function.

Given we've already registered all these routers in the app instance, we'll be skipping the third step.

Testing with Postman

So now it's time we test out our API. Don't forget to restart the application.

I'm using Postman for testing the APIs and responses from different endpoints are as follows:

Create Route

Index Route

Show Route

Update Route

Delete Route


Authentication

The authentication system in this API is a very simple and naive one. It consists of two endpoints: 

  • Register - for creating new users in database.
  • Login - for returning JWTs to users.

There is also a middleware for checking if the user is authenticated or not.

We'll begin by implementing the routes again following our three steps procedure.

Step 1: Open up controllers/auth.ts and put following code in there for register and login routes:

import { Status } from "https://deno.land/x/oak/mod.ts";
import { hash, compare } from "https://deno.land/x/bcrypt/mod.ts";
import { makeJwt, Jose, Payload } from "https://deno.land/x/djwt/create.ts";

import client from "../db/mysql.ts";

export async function register(ctx: any) {
  const body = await ctx.request.body();

  const name = body.value.name;
  const email = body.value.email;
  const password = await hash(body.value.password);

  const result = await client.execute(
    "INSERT INTO users (name, email, password) VALUES (?, ?, ?)",
    [name, email, password],
  );

  ctx.response.status = Status.Created;
  ctx.response.type = "json";
  ctx.response.body = {
    status: "success",
    message: `${result.affectedRows} user registered in database`,
    data: {
      todo: {
        id: result.lastInsertId,
      },
    },
  };
}

export async function login(ctx: any) {
  const body = await ctx.request.body();

  const result = await client.execute(
    "SELECT * FROM users WHERE email = ?",
    [body.value.email],
  );
  const rows: any = result.rows;

  let user: any;

  if (rows.length > 0) {
    const user = {
      id: rows[0].id,
      name: rows[0].name,
      email: rows[0].email,
      password: rows[0].password,
      created_at: rows[0].created_at,
    };

    if (await compare(body.value.password, user.password)) {
      const header: Jose = { alg: "HS256", typ: "JWT" };
      const payload: Payload = {
        id: user.id,
        name: user.name,
        email: user.email,
      };
      const key: string = Deno.env.get("TOKEN_SECRET") ||
        "H3EgqdTJ1SqtOekMQXxwufbo2iPpu89O";

      const token = makeJwt({ header, payload, key });

      ctx.response.status = Status.OK;
      ctx.response.type = "json";
      ctx.response.body = {
        status: "success",
        message: `Logged in with ${body.value.email}`,
        data: { accessToken: token },
      };
    } else {
      ctx.throw(Status.Unauthorized);
    }
  } else {
    ctx.throw(Status.UnprocessableEntity);
  }
}

I must warn you though, this is a perfect example of how not to implement authentication in APIs. There is no conflict checking in the register function, the JWTs never expire, there is no refresh token but for the sake of simplicity, let's roll with it now.

In the register route, we're taking the name, email, password from the user, and creating a new user. Bcrypt is being used for password hashing.

The login route is nothing new until we hit line 47. In this line, we're comparing the password hash with the plain password given by the user. If the passwords match, we generate a long-lived JWT and send it back to the user. If you want to learn more about JWTs, follow this link. Again this is not how you should do authentication in real-life scenarios. It's always better to issue short-lived access token and long-lived refresh token inside a cookie. Also, the key to sign a token comes from an environment variable set inside the docker-compose.yml file.

Step 2: Open up routes/auth.ts and put following code in there:

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

import { register, login } from "../controllers/auth.ts";

const router = new Router();

router.post("/auth/register", register)
      .post("/auth/login", login);

export default router;

We're importing the two controller functions, assigning them as middleware for register and login routes. Both routes are post routes.

Step 3: Open up app.ts and update the code to look like as follows:

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

import logger from "./middleware/logger.ts";
import timer from "./middleware/timer.ts";
import error from "./middleware/error.ts";

import blogs from "./routes/blogs.ts";
import auth from "./routes/auth.ts";

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

app.use(logger);
app.use(timer);
app.use(error);

router.get("/", (ctx) => {
  ctx.response.status = Status.OK;
  ctx.response.type = "json";
  ctx.response.body = {
    status: "success",
    message: "Hello World!",
    data: null,
  };
});

app.use(router.routes())
  .use(blogs.routes())
  .use(auth.routes());

console.log("app running -> http://localhost:3000");
await app.listen({ port: 3000 });

Nothing much has changed except the importing of auth routes at line 8 and registration of them in the app instance at line 29.

Testing with Postman

Again it's time to test out our authentication routes. Restart the application by hitting control + c and running docker-compose up command.

Once the app is running we can test it out:

Register Route

Login Route

Authorization Middleware

So far we've implemented endpoints for regsitering users and generating JWTs for them. Now we need a middleware that will check if the user is authenticated or not in certain routes.

Open up middleware/authorize.ts and put following code in there -

import { Status } from "https://deno.land/x/oak/mod.ts";
import { validateJwt } from "https://deno.land/x/djwt/validate.ts";

export default async (ctx: any, next: any) => {
  const authHeader = ctx.request.headers.get("authorization");

  if (!authHeader) {
    ctx.throw(Status.Unauthorized, "Access Token Missing!");
  } else {
    const token = authHeader.split(" ")[1];

    try {
      const key: string = Deno.env.get("TOKEN_SECRET") ||
        "H3EgqdTJ1SqtOekMQXxwufbo2iPpu89O";

      const { payload }: any = await validateJwt(token, key);

      ctx.request.user = payload;

      await next();
    } catch (err) {
      ctx.throw(Status.Unauthorized);
    }
  }
};

In this middleware, we're checking if the user has sent an access token in the authorization header or not. If yes we validate the token and send the user in his merry way by calling next() method. In case of missing or invalid token, we throw an unauthorized exception.

Keep in mind though, the key used for verifying has to be the same as the key used for signing the token.

Using this middleware is very easy. Assume we want the user to be logged in for creating, updating, and deleting blogs. To do that, open up routes/blogs.ts and update its content as follows:

// previously written codes
import authorize from '../middleware/authorize.ts';

// previously written codes
router.get("/blogs", index)
      .post("/blogs", authorize, store)
      .get("/blogs/:slug", show)
      .put("/blogs/:slug", authorize, update)
      .delete("/blogs/:slug", authorize, destroy);

As you can see, all we gotta do is to import the authorize middleware and pass it as an argument for the routes we want. One thing though, the authorize middleware should come before the controller function because middleware are processed in the order they're registered.

If the user is authenticated the authorize middleware will hand over the request to the next middleware in the stack, otherwise it'll throw an exception. Simple but functional.

Now if we try to access one of these protected routes without an access token we'll be responded with 401 response:

Delete Route wihtout Token

Adding the access token required from login route as bearer token however lets us in:

Delete Route with Token


What Now?

The source code in the master branch is a bit more complicated, with deps.ts convention, versioned module URLs, better abstraction things like that:

GitHub logo fhsinchy / deno-blog

An experimental blogging application developed with Docker, Deno, Oak and MySQL

Code in the master branch is often subjected to change whereas the simplified-tutorial-version branch is almost frozen unless there are any breaking issues. Now that you have a somewhat good understanding of how things work, you are free to explore the master branch as well.

You can also read my follow up article on this topic:

Best of luck for your journey to the Deno Land ✈️

Posted on by:

fhsinchy profile

Farhan Hasin Chowdhury

@fhsinchy

Programmer 👨‍💻 | Voracious Reader 📖 | Video Game Enthusiast 🎮 | Author @freeCodeCamp

Discussion

markdown guide
 

Hey Farhan, great work, and of course very interesting topic. Would you like to integrate your project to our platform and make it scalable, and reusable for you and your future developments - and maybe even for others? You may earn some easy money from it. Hit me up on paul.coch@generato.com or linkedin.com/in/paul-coch