Despite the success of Node.js and the great effort done by the community since its creation, Ryan Dahl the creator of the famous javascript runtime, decided however in 2018 to design a new secure runtime for javascript built on V8, Rust, Typescript and Tokio (event loop). He declared that there are some design mistakes in Node.js and he regrets about them, then he has created deno which takes into consideration those anomalies.
If you are curious about the issue, you can check his presentation in JSConf EU conference in 2018.
The purpose of this article is:
- To create an api to manage employees.
- To create environment variables using denv.
- To implement controllers for the api.
- To communicate with a database (mongodb) using deno_mongo.
- To use a simple deno framework called abc.
First of all, you need to install deno in your machine, and according to your OS you can choose the suitable command line, check install section for more information.
PS: At the moment of writing this article, we use deno v1.0.3 to create our api.
Let's launch work
In order to facilitate the design of our server, we need a framework (let's say the equivalent of express in Node.js). In our case, we choose abc a simple deno framework to create web applications (abc is not the only framework there are others like alosaur, espresso, fen, oak, etc...).
First of all, we start by decalring our environment variables in .env
file:
DB_NAME=deno_demo
DB_HOST_URL=mongodb://localhost:27017
Then, we create an error middleware to handle errors catched in the controllers:
import { MiddlewareFunc } from "https://deno.land/x/abc@v1/mod.ts";
export class ErrorHandler extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
export const ErrorMiddleware: MiddlewareFunc = (next) =>
async (c) => {
try {
await next(c);
} catch (err) {
const error = err as ErrorHandler;
c.response.status = error.status || 500;
c.response.body = error.message;
}
};
Then, we create server.ts
(the main file of our server):
import { Application } from "https://deno.land/x/abc@v1/mod.ts";
import "https://deno.land/x/denv/mod.ts";
import {
fetchAllEmployees,
createEmployee,
fetchOneEmployee,
updateEmployee,
deleteEmployee,
} from "./controllers/employees.ts";
import { ErrorMiddleware } from "./utils/middlewares.ts";
const app = new Application();
app.use(ErrorMiddleware);
app.get("/employees", fetchAllEmployees)
.post("/employees", createEmployee)
.get("/employees/:id", fetchOneEmployee)
.put("/employees/:id", updateEmployee)
.delete("/employees/:id", deleteEmployee)
.start({ port: 5000 });
console.log(`server listening on http://localhost:5000`);
In the first line, you will find that we import modules directly from the internet using an url. The first time you need to import a module, deno fetches it then download cache the dependencies.
The second line calls denv in order to load the enviornment variables from the .env
file.
The rest of code is almost similar to express, nothing special.
Now, we need to configure our database to interact with the server. Fortunately, there is deno_mongo a MongoDB database driver developed for deno. It is under construction and still does not contain the different methods of mongodb driver but it is ok for a simple demo.
import { init, MongoClient } from "https://deno.land/x/mongo@v0.8.0/mod.ts";
class DB {
public client: MongoClient;
constructor(public dbName: string, public url: string) {
this.dbName = dbName;
this.url = url;
this.client = {} as MongoClient;
}
connect() {
const client = new MongoClient();
client.connectWithUri(this.url);
this.client = client;
}
get getDatabase() {
return this.client.database(this.dbName);
}
}
const dbName = Deno.env.get("DB_NAME") || "deno_demo";
const dbHostUrl = Deno.env.get("DB_HOST_URL") || "mongodb://localhost:27017";
const db = new DB(dbName, dbHostUrl);
db.connect();
export default db;
We create a DB class which allows to start a database connection, so we create a new instance with DB_NAME
and DB_HOST_URL
.
Then, we write our controllers, let's start by createEmployee
:
import { HandlerFunc, Context } from "https://deno.land/x/abc@v1/mod.ts";
import db from '../config/db.ts';
const database = db.getDatabase;
const employees = database.collection('employees');
interface Employee {
_id: {
$oid: string;
};
name: string;
age: number;
salary: number;
}
interface NewEmployee {
name: string;
age: number;
salary: number;
}
export const createEmployee: HandlerFunc = async (c: Context) => {
try {
if (c.request.headers.get("content-type") !== "application/json") {
throw new ErrorHandler("Invalid body", 422);
}
const body = await (c.body<NewEmployee>());
if (!Object.keys(body).length) {
throw new ErrorHandler("Request body can not be empty!", 400);
}
const { name, salary, age } = body;
const insertedEmployee = await employees.insertOne({
name,
age,
salary,
});
return c.json(insertedEmployee, 201);
} catch (error) {
throw new ErrorHandler(error.message, error.status || 500);
}
};
The mongo driver returns an object containing only the $oid
attribute (I hope it will be updated in the next versions of the module).
To fetch all the employees, we call:
export const fetchAllEmployees: HandlerFunc = async (c: Context) => {
try {
const fetchedEmployees: Employee[] = await employees.find({name:{$ne: null}});
if (fetchedEmployees) {
const list = fetchedEmployees.length
? fetchedEmployees.map((employee) => {
const { _id: { $oid }, name, age, salary } = employee;
return { id: $oid, name, age, salary };
})
: [];
return c.json(list, 200);
}
} catch (error) {
throw new ErrorHandler(error.message, error.status || 500);
}
};
For fetching a given employee by id, fetchOneEmployee
will be called:
export const fetchOneEmployee: HandlerFunc = async (c: Context) => {
try {
const { id } = c.params as { id: string };
const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });
if (fetchedEmployee) {
const { _id: { $oid }, name, age, salary } = fetchedEmployee;
return c.json({ id: $oid, name, age, salary }, 200);
}
throw new ErrorHandler("Employee not found", 404);
} catch (error) {
throw new ErrorHandler(error.message, error.status || 500);
}
};
Update a given employee:
export const updateEmployee: HandlerFunc = async (c: Context) => {
try {
const { id } = c.params as { id: string };
if (c.request.headers.get("content-type") !== "application/json") {
throw new ErrorHandler("Invalid body", 422);
}
const body = await (c.body()) as {
name?: string;
salary: string;
age?: string;
};
if (!Object.keys(body).length) {
throw new ErrorHandler("Request body can not be empty!", 400);
}
const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });
if (fetchedEmployee) {
const { matchedCount } = await employees.updateOne(
{ _id: { "$oid": id } },
{ $set: body },
);
if (matchedCount) {
return c.string("Employee updated successfully!", 204);
}
return c.string("Unable to update employee");
}
throw new ErrorHandler("Employee not found", 404);
} catch (error) {
throw new ErrorHandler(error.message, error.status || 500);
}
};
The driver here returns an object containing:
- matchedCount
- modifiedCount
- upsertedId
Finally, to delete an employe:
export const deleteEmployee: HandlerFunc = async (c: Context) => {
try {
const { id } = c.params as { id: string };
const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });
if (fetchedEmployee) {
const deleteCount = await employees.deleteOne({ _id: { "$oid": id } });
if (deleteCount) {
return c.string("Employee deleted successfully!", 204);
}
throw new ErrorHandler("Unable to delete employee", 400);
}
throw new ErrorHandler("Employee not found", 404);
} catch (error) {
throw new ErrorHandler(error.message, error.status || 500);
}
};
Let's start our server now:
deno run --allow-write --allow-read --allow-plugin --allow-net --allow-env --unstable ./server.ts
In order, to guarantee a secure exection of the program, deno blocks every access to disk, networks or environment variables. Therefore, to allow the server to be executed, you need to add the following flags:
- --allow-write
- --allow-read
- --allow-plugin
- --allow-net
- --allow-env
Probably, you will ask yourself " how will I know which flags I have to add to execute the server?". Don't worry you will get a messge in the console log asking you to add a given flag.
Now, you will see something similar to this in your terminal:
INFO load deno plugin "deno_mongo" from local "~/.deno_plugins/deno_mongo_40ee79e739a57022e3984775fe5fd0ff.dll"
server listening on http://localhost:5000
Summary
In this article, we :
- Created an employees' api using Deno.
- Created a connection to a mongodb database using mongo driver for deno.
- Used the abc framework to create our server.
- Declared the environment variables using denv.
You probably realized that we :
- Don't need to initialize a package.json file or install modules under node_modules.
- Import modules directly using urls.
- Add flags to secure the execution of the program.
- Don't install typescript locally because it's compiled in Deno.
That's it folks, don't hesitate to leave a comment if there is any mistake, if you have a question or a suggestion. If you like the article, don't forget to click the heart button or tweet it ;).
Code
You can find the code here: https://github.com/slim-hmidi/deno-employees-api
Top comments (9)
I'm seeing the following error when I try to implement the error handler middleware:
error: TS2345 [ERROR]: Argument of type 'MiddlewareFunc' is not assignable to parameter of type 'Middleware, Context>>'.
Types of parameters 'h' and 'context' are incompatible.
Type 'Context>' is not assignable to type 'HandlerFunc'.
Type 'Context>' provides no match for the signature '(c: Context): unknown'.
app.use(ErrorMiddleware);
Any thoughts on this ?
Thanks for your comment. Can you please tell me which version of deno are you using?
Upgrading Deno did fix the issue. I must have been on a lower version. I forgot to check before upgrading
Thanks for the article. I did a similar one using oak instead of abc. Oak looks promising too.
Let me know what you think: github.com/hagopj13/deno-books-api
Thanks, great job for your api. I will try oak with another app.
Great article and very well explained for this new javascript environment. Only one tiny comment await init() is not required on the last version of deno-mongo plugin.
Thanks for your comment. I updated deno to v1.0.3 and mongo to v0.8.0. I tried to update to the latest version for each module but they aren't stable.
Initialising the plugin with 'init()' now requires a 'releaseUrl: string' parameter.
What should go in this string? Where can I find this information?
Thanks.
Thanks for your comment. I updated deno to v1.0.3 and mongo to v0.8.0 and no need for the
init()
now. I tried to update to the latest version for each module but they aren't stable.