Deno is a new Javascript runtime, created by Ryan Dahl. Version 1.0 released on 13th May 2020. The most notable Deno features are security and URL importing. By default Deno blocks access to any system resources, unless it’s explicitly asked. Deno also doesn’t have any package manager, all libraries are loaded from CDN. All these unique features of Deno, make me curious. Hence, I decided to get my hands dirty with Deno. I’ve prepared a very simple RESTful application. In this article, you will learn how to build REST APIs with Deno and Oak middleware.
The famous User application
Since I don’t want to overwhelm you with complex business logic, I stick to the famous User API that I covered numerous times on this website. Essentially, we are going to build a simple CRUD RESTful User app that has no persistence, means for now we store all data in an array for simplicity’s sake.
We are going to create the following APIs:
GET /
– displays welcome pageGET /v1/users
– returns a list of usersGET /v1/users/:id
– gets a user by idPOST /v1/users
– creates a new userPUT /v1/users/:id
– update an existing userDELETE /v1/users/:id
– deletes a user by id
As you can see, the APIs are very rudimentary and the only remaining challenge is how to implement the project itself.
Project scaffolding
Remember we don’t NPM or any package manager, so we can’t rely on them to scaffold our project. All we have to do is to create a simple .js
file and start coding right away.
Some may say,
But wait what about dependencies? Don’t we suppose to download them?
The answer is:
Yes and no!
We just need to add their CDN URLs using the import
statement and then Deno takes care of the rest. Sounds confusing? Don’t worry. It will become crystal clear once we start coding.
Implementing the APIs
Deno has a built-in HTTP server but doesn’t have all features to build RESTful APIs. To make our task easier, we use Oak library. Oak is a middleware framework for Deno’s HTTP server, including a router middleware. You can see it as Express in Node.js world 🙂
Root endpoint implementation
First, let’s create the index.js
file and write the following couple of lines of code.
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
const app = new Application();
router.get("/", (context) => {
context.response.body = "Welcome to Oak example with Deno!";
});
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8080 });
The first line imports the Application
and Router
from Oak
. As you can see they are loaded from CDN. The dependencies will be downloaded (first time) automatically if they don’t already exist on your computer.
In the second line we create an instance of the router
. Then we add the /
resources that greet users with a welcome message.
The input of the method is the context
object. This object contains all we need for this project implementation such as request
, request.body
, request.params
, etc. I highly recommend you to check the Oak documentation.
The rest starts an HTTP server and listens to port 8080.
Now let’s start to run the project. Open your terminal and type,
$ deno run index.js
Wait what!!?? You got this error message:
error: Uncaught PermissionDenied: network access to "0.0.0.0:8080", run again with the --allow-net flag
at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10)
at Object.listen ($deno$/ops/net.ts:51:10)
at listen ($deno$/net.ts:152:22)
at Application.serve (https://deno.land/[email protected]/http/server.ts:261:20)
at Application.listen (https://deno.land/x/oak/application.ts:250:20)
at file:///home/kixz/Desktop/index.js:91:11
Remember at the beginning of the article I mentioned Deno is secured and any permission should be asked explicitly. This is an example of it. We have to ask explicitly to get access to the network. Hence, we need to run this command.
$ deno run --allow-net index.js
GET /v1/users
implementation
Since the project foundation is ready, implementing the rest of the code is very easy.
Let’s create a dummy array to act as temporary storage and hold users.
const dummyUsers = [
{ id: 1, firstName: "John", lastName: "Wick", age: 41 },
{ id: 2, firstName: "Joe", lastName: "Doe", age: 35 },
{ id: 3, firstName: "Brad", lastName: "Pitt", age: 50 }
];
Then, we need to have another get
implemented like this,
router.get("/v1/users", (context) => {
context.response.body = { users: dummyUsers };
})
Now if you hit localhost:8080/v1/users
, you should get a list of users stored in dummyUsers
constant.
GET /v1/users/:id
implementation
For this endpoint, all we have to do is to filter the dummyUsers
by id
and return the user if it exists. Otherwise, throw 404
.
For that we create a function called getById
.
const getById = (id) => {
return dummyUsers.filter(user => user.id == id);
}
And then implement the endpoint,
router.get("/v1/users/:id", (context) => {
if (context.params && context.params.id && !isNaN(parseInt(context.params.id))) {
const result = getById(context.params.id);
if (result.length < 1) {
context.response.status = 404;
context.response.body = { error: `User ${context.params.id} not found` };
} else {
context.response.body = result[0];
}
} else {
context.response.status = 400;
context.response.body = { error: "Id must be number" };
}
})
The code has some additional checking to ensure the passed parameter exists and it’s a number. The rest is the same as explained before.
POST /v1/users/
implementation
We expect the API consumer to pass a user and it should be added to the dummyUsers
array. We don’t have any checks for duplication. The only restriction is age
that must be a number and the rest of the fields (firstName
, lastName
) should be present.
To check and get the request body we can rely on context.request.hasBody
and context.request.body
functions. The latter returns a promise which means the endpoint should be async
.
router.post("/v1/users", async (context) => {
if (!context.request.hasBody) {
context.response.status = 400;
context.response.body = { error: "Request body cannot be empty" };
} else {
const { firstName, lastName, age } = (await context.request.body(true)).value
const ageInt = parseInt(age);
if (firstName && lastName && ageInt) {
dummyUsers.push({ id: dummyUsers.length + 1, firstName: firstName, lastName: lastName, age: ageInt });
context.response.status = 201;
context.response.body = dummyUsers[dummyUsers.length - 1];
} else {
context.response.status = 400;
context.response.body = { error: "Invalid payload's provided" };
}
}
})
The first block does some validations to ensure the body exists. Then the value of the body is read and finally after verification a new record is added to the dummyUsers
array. Otherwise, it returns 400.
PUT /v1/users/:id
implementation
Similar to the POST endpoint we need to read request.body
which makes the implementation quite similar to the previous section,
router.put("/v1/users/:id", async (context) => {
if (!context.params || !context.params.id ||
isNaN(parseInt(context.params.id) || !context.request.hasBody)) {
context.response.status = 400;
context.response.body = { error: "Invalid request" };
} else {
const result = getById(context.params.id);
if (result.length < 1) {
context.response.status = 404;
context.response.body = { error: `User ${context.params.id} not found` };
} else {
const id = context.params.id;
const { firstName, lastName, age } = (await context.request.body()).value
const ageInt = parseInt(age);
if (firstName && lastName && ageInt) {
dummyUsers.splice(id - 1, 1, { id: id, firstName: firstName, lastName: lastName, age: ageInt });
context.response.status = 200;
context.response.body = dummyUsers[id-1];
} else {
context.response.status = 400;
context.response.body = { error: "Invalid payload's provided" };
}
}
}
})
To update an element in the array the code utilizes the splice
function.
DELETE /v1/users/:id
implementation
This is the last endpoint. We can copy GET /v1/users/:id
and do some slight modifications as follows,
router.delete("/v1/users/:id", (context) => {
if (context.params && context.params.id && !isNaN(parseInt(context.params.id))) {
dummyUsers.splice(context.params.id - 1, 1);
context.response.status = 204;
} else {
context.response.status = 400;
context.response.body = { error: "Id must be number" };
}
})
That’s all you need to build Rest APIs with Deno and Oak 😀
You can find the complete workable example on my GitHub at the link below,
https://github.com/kasramp/deno-oak-rest
The entire project is a single file. Isn’t it fantastic? No package.json
, and package-lock.json
are required. We can take the Deno executable (again it’s a single file) and the project file and run it anywhere without problems.
All it takes to run the project are two files (Deno executable, and the project implementation). This gives a huge advantage if we need to run the project in the container or deploy it to a cloud environment.
If you enjoyed this article, don’t forget to check my previous Deno related article “Is Node.js dying?“.