This content originally appeared on DEV Community and was authored by NARUHODO
Deno is a JavaScript and TypeScript runtime build in Rust. It was created by Ryan Dahl, who is also the original creator of Node.js.
Eventually Deno might take over Node.js but at the moment, it's not there yet. Node.js is too popular and is already used in many projects. It has a much bigger community and much more modules/packages available.
Deno does not provide a strong enough incentive for companies to decide to make the switch. At least not yet.
Performance-wise it is comparable to Node.js, another reason not to switch.
So why am I writing an article about it? And what does Deno have that Node.js doesn't?
Well, I believe that if Ryan Dahl decided to build a new JS runtime, he must have had a good reason. He actually gave a talk about this.
In his talk he mentions multiple things he regrets about Node.js. Two big ones are that Node.js is insecure, by default it has access to everything (filesystem, network, etc) and NPM, the default package manager (bundled with Node.js) is centralized and private.
Deno by default does not have access to your network or your filesystem. It also allows you to use modules from anywhere without relying on a 3rd party tool, you just need to provide the full URL pointing to the module.
Another nice thing about Deno is that it runs TypeScript out of the box, you don't need a transpilation step nor a complicated configuration.
What are we going to build?
Now that I've introduced Deno, I will show you how to build a simple API with it. I will reproduce the example I made in my Rust guide.
We will create an API with the following endpoints
GET /
This endpoint will simply return a text messageHello World!
.GET /items
This endpoint will return the list of items we have in the database in a JSON format.POST /items
This endpoint will allow us to create a new item in the database.
It expects a body of type:
type Body = {
name: string;
price: number;
};
The POST /items
endpoint will be protected with an authorization middleware using JWT (JSON Web Token).
We will also setup velociraptor
which allows us to define scripts in a file to run Deno commands more easily. It is a bit similar to the scripts
property in Node.js's package.json
.
Prerequisites
To be able to follow the guide, you will need to have Deno installed. You will need velociraptor
as well, you can go ahead and install it by following the official documentation.
You will also need to have a MongoDB running. If you're not sure how to install MongoDB, there are 3 ways I can think of:
You can either install MongoDB on your machine. Follow the official documentation to do so.
If you have Docker, you can run a MongoDB container. Check out the
mongo
image documentation. This is the command I use to run themongo
container:
docker run -d -p 27017:27017 --name mongo mongo
- If you prefer to avoid installing anything (MongoDB/Docker), you can create a free cluster on MongoDB Atlas.
Set up the environment
We need to add a couple environment variables in a .env
file. If you haven't done it yet, create a new directory for our project and move inside it:
mkdir deno-example && cd deno-example
Then create the .env
file
touch .env
Add the following two variables inside the file
MONGODB_URI=mongodb://127.0.0.1:27017
JWT_SECRET=my_secret
Adapt the MONGODB_URI
if necessary. For the JWT_SECRET
variable, change it to whatever you like.
It is good practice to have a .env.example
as well, to document the environment variables. Go ahead and create that file
touch .env.example
Add the following in it
MONGODB_URI= ** MongoDB connection string (e.g. mongodb://127.0.0.1:27017)
JWT_SECRET= ** Secret to encode/decode JWTs
If you're using Git, make sure to add .env
inside your .gitignore
.
Add the dependencies
In Deno you can import the modules directly in your code by using their URL. To make things a bit cleaner and avoid having to copy and paste modules URLs all the time, we can instead create a import-map.json
file where we define our dependencies and give them an alias.
Create the import-map.json
file at the project root
touch import-map.json
Add the following
{
"imports": {
"oak": "https://deno.land/x/oak@v7.7.0/mod.ts",
"denodb": "https://deno.land/x/denodb@v1.0.38/mod.ts",
"dotenv": "https://deno.land/x/dotenv@v2.0.0/mod.ts",
"djwt": "https://deno.land/x/djwt@v2.2/mod.ts"
}
}
We can now import these modules using their alias: oak
, denodb
, dotenv
and djwt
.
Let's review the dependencies:
-
oak
is our web library / framework, it is heavily inspired ofkoa
the "successor" ofexpress
in Node.js. -
denodb
is an ORM supporting multiple databases including MongoDB. -
dotenv
is similar to the module of the same name in Node.js. It allows to inject environment variables from a file (by default.env
). -
djwt
is a JWT encoding/parsing library.
Define the scripts
Now we will define two scripts to launch the API more easily.
Create a scripts.json
file at the root of the project
touch scripts.json
And add the following
{
"imap": "import-map.json",
"scripts": {
"dev": {
"cmd": "src/main.ts",
"watch": true,
"allow": ["net", "read", "env"]
},
"start": {
"cmd": "src/main.ts",
"watch": false,
"allow": ["net", "read", "env"]
}
}
}
I've configured a global imap
(short for import map) that will be used by all the scripts. Then I've defined two scripts, dev
and start
. They both do the same thing except that dev
will watch for file changes.
The API will need access to the network to communicate with MongoDB and receive requests from the outside. It will also need read access to the filesystem to read the .env
file. And finally it needs access to the environment to be able to add the variables defined in the .env
file.
Let's code
First, create the src
directory
mkdir src
Then create a main.ts
file inside that directory
touch src/main.ts
From now on all the code that I will show should go inside that src/main.ts
file. Of course in a real project you should split the code and logic in different subdirectories.
Let's start by importing the modules. Add the following at the top of the file
import { Application, Router, RouterMiddleware } from "oak";
import { Database, DataTypes, Model, MongoDBConnector } from "denodb";
import * as dotenv from "dotenv";
import * as jwt from "djwt";
Nothing complicated so far.
Now we will use dotenv
to inject the environment variables from the .env
file.
// Import environment variables from the ".env" file
// and ensure all the variables are defined
dotenv.config({ safe: true, export: true });
Using the safe
option ensure that our .env
matches variables defined in .env.example
. The export
option is to inject the variable inside Deno's environment (Deno.env
).
To ensure the environment variables are set up correctly I will double check that Deno.env
contains the variables defined in the .env
file like so
const missingEnvVars = [];
// Double check that the the "MONGODB_URI" is defined
if (typeof Deno.env.get("MONGODB_URI") !== "string") {
missingEnvVars.push("MONGODB_URI");
}
// Double check that the the "JWT_SECRET" is defined
if (typeof Deno.env.get("JWT_SECRET") !== "string") {
missingEnvVars.push("JWT_SECRET");
}
// If any environment variable is missing throw an error
if (missingEnvVars.length > 0) {
throw new Error(
`The following environment variables are missing: ${missingEnvVars.join(
", "
)}`
);
}
If any environment variable is missing I throw an error and the process stops.
Now I will set up the database. First I need to define the Item
model
// Define the Item model from MongoDB
class Item extends Model {
static table = "items";
static timestamps = true;
static fields = {
_id: {
primaryKey: true,
},
name: DataTypes.STRING,
value: DataTypes.DECIMAL,
};
}
The Item
class extends the Model
class from the denodb
module. The table
static variable is in our case the name of the collection in MongoDB.
The timestamps
static variable indicates whether or not to automatically add timestamps (createdAt
and updatedAt
).
The fields
static variable defines the structure of an Item
document. In MongoDB, we need a _id
which is the primary key. Then we have the two fields, name
of type string and price
of type number.
Let's connect and initialize the database
// Create a MongoDB connection
const connector = new MongoDBConnector({
uri: Deno.env.get("MONGODB_URI") as string,
database: "deno-example",
});
// Instantiate the database
const db = new Database(connector);
// Define the database models
db.link([Item]);
// Setup the models in the database
db.sync();
Now, we will define our three controllers, the first one will simply return Hello World!
as plain text
// Controller for the GET / route that simply returns "Hello World!"
const helloController: RouterMiddleware = (ctx) => {
ctx.response.body = "Hello World!";
};
The next one will retrieve all the items from the database and send them in JSON format
// Controller for the GET /items route to retrieve all the items from the database
const getItemsController: RouterMiddleware = async (ctx) => {
// Retrieve all the items from the database
const items: Item[] = await Item.all();
// Send the items in JSON format
ctx.response.body = items.map((item) => ({
id: item._id,
name: item.name,
price: item.price,
}));
};
The last one will allow to create new items in the database
// Controller for the POST /items route to create a new item in the database
const postItemController: RouterMiddleware = async (ctx) => {
// If there is no body return 400
if (ctx.request.hasBody === false) {
ctx.response.status = 400;
ctx.response.body = { error: "Missing body" };
return;
}
let body: { price: number; name: string };
try {
const b = ctx.request.body();
// If the body is no in JSON format return 400
if (b.type !== "json") {
ctx.response.status = 400;
ctx.response.body = { error: "Body should be in JSON format" };
return;
}
body = await b.value;
} catch (_err) {
// If there was an error parsing the body return 400
ctx.response.status = 400;
ctx.response.body = { error: "Could not parse the request's body" };
return;
}
// Extract "price" and "name" properties from the body
const { price, name } = body;
// If the "price" is not a number return 400
if (typeof price !== "number") {
ctx.response.status = 400;
ctx.response.body = {
error: 'Property "price" is incorrect or missing',
};
return;
}
// If the "name" is not a string return 400
if (typeof name !== "string") {
ctx.response.status = 400;
ctx.response.body = {
error: 'Property "name" is incorrect or missing',
};
return;
}
// Create a new item in the database with the values from the body
await Item.create({ name, price });
// Return 200
ctx.response.status = 200;
};
I've added comments in the controllers' code so that you understand what I'm doing, but it's pretty straightforward.
Unlike in my Rust tutorial, this time I made sure to validate the request's body in the postItemController
.
We have one extra middleware to create, the authorization middleware. Here is the code
// Authorization middleware that checks that
// the "Authorization" header is present
// and that the token is valid
const authMiddleware: RouterMiddleware = async (ctx, next) => {
// Retrieve the request's headers
const headers = ctx.request.headers;
// Attempt to get the "Authorization" header if it exists
const authorizationHeader = headers.get("Authorization");
// If the "Authorization" header is missing
// or if it doesn't start with the prefix "Bearer " return 401
if (
authorizationHeader === null ||
authorizationHeader.startsWith("Bearer ") === false
) {
ctx.response.status = 401;
return;
}
// Extract the JWT from the header
const token = authorizationHeader.substr("Bearer ".length);
// Verify that the JWT is valid, if not return 401
try {
await jwt.verify(token, Deno.env.get("JWT_SECRET") as string, "HS256");
} catch (_err) {
ctx.response.status = 401;
return;
}
// Token is valid, proceed with the next middleware
await next();
};
Again, there are comments to help you understand. I'm checking that the Authorization
header is present and starts with the prefix Bearer
. Then I validate the token using the djwt
module.
If anything goes wrong the middleware responds with the HTTP status code 401
for Unauthorized
. Otherwise it calls the next
function that simply runs the next middleware in the chain.
We just have to glue it all together now
// Create an "oak" app
const app = new Application();
// Create a router
const router = new Router();
router
.get("/", helloController)
.get("/items", getItemsController)
.post("/items", authMiddleware, postItemController);
// Add router's routes to the "oak" app
app.use(router.routes());
// Register middleware that automatically returns 405 or 501 when appropriate
app.use(router.allowedMethods());
// Start the server on port 8080
await app.listen({ port: 8080 });
I created the oak
app and a new router
. I defined the three routes. For the POST /items
route I adde the authMiddleware
to ensure that only users with the token can create items.
Finally I simply added the router's routes to the oak
app and started the server.
Let's try out the API
We can now try out the API! Run it using
vr start
The API should be running on port 8080
.
Send a GET
request to /
. You should receive a response with the HTTP status code 200
and a plain text message Hello World!
.
Now let's try to create an item. Send a POST
request to /posts
with the following body
{
"name": "coffee",
"price": 2.5
}
You will also need to add an authorization header to the request with the Bearer
prefix. You can generate the JWT on jwt.io using the secret you defined in the .env
file. Make sure to change the payload to an empty JSON object {}
.
You should receive a response with the HTTP status code 200
and no content.
If you try sending the POST
request without the authorization header or with an invalid token you will receive a HTTP status code 401
instead.
Finally send a GET
request to /items
. You should receive a response with the HTTP status code 200
and a list with the item you just created like so
[
{
"name": "coffee",
"price": 2.5
}
]
In conclusion
That's it! I hope you enjoyed the article and that it was helpful :)
You can find the the full example of this walk-through on GitHub: https://github.com/ncribt/deno-example
Bonus: Set up Visual Studio Code for Deno
If you're using Visual Studio Code, I would advise you to install the following extensions
And add the following to your settings
{
"editor.defaultFormatter": "denoland.vscode-deno",
"deno.enable": true,
"deno.lint": true,
"deno.importMap": "./import-map.json",
"deno.suggest.imports.hosts": {
"https://deno.land": true
}
}
Discover more posts from me on my personal blog: https://naruhodo.dev
This content originally appeared on DEV Community and was authored by NARUHODO
NARUHODO | Sciencx (2021-07-08T07:24:13+00:00) An introduction to Deno. Retrieved from https://www.scien.cx/2021/07/08/an-introduction-to-deno/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.