Deno REST — A Boilerplate for Deno RESTful APIs (Oak, MongoDB, JWT)

Vicky Gonsalves
Webtips
Published in
4 min readSep 11, 2020

Deno — A very young modern, and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust, and Tokio.

I have built a quickstart boilerplate for RESTful APIs using this fantastic new technology.

Deno artwork

The full project is located at:

https://github.com/Prolifode/deno_rest.git

Let’s learn more about this project:

This boilerplate is based on Deno v 1.34.3

Pre-requisites:

Install / Upgrade

Using Deno:

deno upgrade --version 1.34.3

With Shell:

curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.34.3

With PowerShell:

$v="1.34.3"; iwr https://deno.land/x/install/install.ps1 -useb | iex

Features

  • Model, Controller, Service-based project structure
  • MongoDB
  • JWT authentication
  • User authorization
  • CORS
  • environment management using .env
  • Request validation
  • Error Handling
  • Database seeding
  • User Roles and Rights
  • Password Hashing
  • Denon Integration

Libraries Used

  • Oak — A middleware framework for Deno’s net server
  • deno_mongo — MongoDB driver for Deno
  • cors — Deno.js CORS middleware
  • djwt — To make JSON Web Tokens in Deno. Based on JWT and JWS specifications.
  • yup — Schema builder for value parsing and validation

Getting Started

Clone this repository to your local machine

git clone https://github.com/vicky-gonsalves/deno_rest.git

Directory Structure

|   .gitignore
| app.ts
| denon.json
| deps.ts
| docker-compose.dev.yml
| docker-compose.test.yml
| docker-compose.yml
| Dockerfile
| LICENSE
| import_map.json
| scripts.config.ts
| deno.lock
| README.md
| seed.ts
| tsconfig.json
+---.circleci
| config.yml
|
+---config
| config.ts
| roles.ts
|
+---controllers
| auth.controller.ts
| user.controller.ts
|
+---data
| users.json
|
+---db
| db.ts
|
+---environments
| .env.development
| .env.example
| .env.test
|
+---helpers
| hash.helper.ts
| jwt.helper.ts
|
+---middlewares
| auth.middleware.ts
| errorHandler.middleware.ts
| logger.middleware.ts
| validate.middleware.ts
|
+---models
| token.model.ts
| user.model.ts
| user_history.model.ts
|
+---routers
| auth.router.ts
| default.router.ts
| index.ts
| user.router.ts
|
+---services
| auth.service.ts
| token.service.ts
| user.service.ts
|
+---tests
| +---fixtures
| | users.fixtures.ts
| |
| +---integration
| | | app.test.ts
| | |
| | +---users
| | user-get.test.ts
| | user-post.test.ts
| |
| +---utils
| utils.ts
|
+---types
| types.interface.ts
|
+---validations
| auth.validation.ts
| user.validation.ts

Setup

Set environments

Review .env/.env.example file and create required .env file suitable to your needs. For example: for a development environment create a file .env.development under .env directory for test environment create a file .env.test under .env directory and add necessary variables.

Install Deno

If it's your first run, please install denon from https://deno.land/x/denon

Install Dependencies

To install dependencies, run the following command in your terminal. Note: Terminal path must be the project directory’s root path

deno cache --reload --unstable --lock-write --lock=lock.json ./deps.ts

OR run the reload_deps.sh file from the project directory

This will automatically download all the dependencies and update lock.json file

SEED Database

If you need to seed (run migrations) database initially, Simply enable SEED=true in your .env file You can add or edit any seed file under data directory. A basic example of a seed file is provided in data/users.json

Manually control Individual seed file

Logic to control seed is located in seed.ts where you can add multiple seed files as follow:

const seedCollections: Array<Record<string, boolean>> = [
{ users: true }, // collection_name: boolean
{ posts: true },
{ comments: false},
...
...

Note: file name must be in collection_name.json pattern

RUN

In your project root open terminal and run the following command to run the project

denon start

User Roles and Rights

User roles are saved in config/roles.ts file as:

export const roles = ["user", "admin"];

and Rights are saved as:

roleRights.set(roles[0], [
"getMe",
]);
roleRights.set(roles[1], [
"getMe",
"getUsers",
"manageUsers",
]);

You can add/edit roles and rights as per your requirements.

API Routes

All routes are stored under routers directory Below is the example of /api/users route. This route is JWT protected In user.router.ts:

...
/** JWT protected route */
router.post(
"/api/users", // route
auth("manageUsers"), // Auth Guard based on djwt
validate(createUserValidation), // Yup based validation
UserController.create, // Controller Function
);
...
...

Non-JWT protected route:

router.post(
"/api/auth/login",
validate(loginValidation), // Yup based validation
AuthController.login,
);

Models

All models are under models directory example of User Model:

import db from "../db/db.ts";export interface UserSchema {
_id?: { $oid?: string };
name: string;
email: string;
password: string;
role: string;
isDisabled: boolean;
createdAt?: Date;
updatedAt?: Date;
}
export const User = db.getDatabase.collection<UserSchema>("users");

Controllers

Controllers are saved under controllers directory Example of User Controller:

...
class UserController {
/**
* Create User function * @param request
* @param response
* @returns Promise<void>
*/public static async create(
{ request, response }: RouterContext,
): Promise<void> {
const body = request.body();
let {
name,
email,
password,
role,
isDisabled,
} = await body.value;
log.debug("Creating user");
response.body = await UserService.createUser({
name,
email,
password,
role: role || roles[0],
isDisabled: typeof isDisabled === "boolean" ? isDisabled : false,
});
}
...
...

Services

All Services are under services directory Example of User service:

class UserService {
/**
* Create user Service * @param options
* @returns Promise<ObjectId | Error> Returns Mongo Id of user document
*/ public static async createUser(
options: CreateUserStructure,
): Promise<ObjectId | Error> {
const { name, email, password, role, isDisabled } = options;
const hashedPassword = await HashHelper.encrypt(password);
const createdAt = new Date();
const user: ObjectId = await User.insertOne(
{ name, email, password: hashedPassword, role, isDisabled, createdAt },
);
if (!user) {
log.error("Could not create user");
return throwError({
status: Status.BadRequest,
name: "BadRequest",
path: "user",
param: "user",
message: `Could not create user`,
type: "BadRequest",
});
}
return user;
}
...
...

Happy Coding !

--

--

Vicky Gonsalves
Webtips
Writer for

Javascript Nerd.. Traveller, Beer & Scotch lover.. Strongest Capricorn.. Ends year with birthday hangover..