Deno is a recently launched runtime environment for JavaScript and TypeScript used for building modern server-side applications.
In this article, you will learn how to build a backend server with Deno. I will show you how to create a live server, create API routes, store environment variables, read and write files in file systems, and create JWT tokens.
We will bring it all together at the end of the article to form your first Deno application.
Deno: Brief Overview
Deno uses the V8 JavaScript Engine, just like Node, which allows JavaScript to run on a computer.
This tool was launched by Ryan Dahl, the creator of Node.js. According to Ryan, he created Deno to fix the problems (or rather, the mistakes he made) in
Node. These problems include security issues and module management (downloading, importing, etc.), to name a few.
Differences Between Deno and Node
For the most part, Deno is very similar to Node (its name even includes the same characters as Node 😅). It uses JavaScript code and ES6 syntax. But, here are some of the ways Deno works differently:
-
It comes with built-in support for TypeScript.
-
It has a standard module library, maintained by the Deno team, which includes many functionalities you may need.
-
It doesn’t use a
package.json
file to manage packages. Packages are also not installed in anode_modules
directory. To install a package in your Deno application, you only need to import the URL in your file from wherever it is hosted. It could be from the standard library or any file on the web:js123import { package } from 'https://.../file.ts' // use package for anything
After installing the package for the first time, Deno caches it so you do not have to make install requests for subsequent usage of the package.
-
It is more secure. It works with the idea of permissions, which means that some scripts (especially those that attempt to do something with your computer's resources) would need to be permitted by you to access certain resources before they can work. This approach makes it harder for malicious scripts to execute on their own.
There's more to Deno, and I will explain as I walk you through building your first Deno application.
Building Your First Deno Application
Developer Setup
To get started, you first need to install Deno. Check out the installation documentation for your device.
After installing Deno, create a new folder called deno-tutorial. This folder can be anywhere you want it on your device. Afterward, open the folder in your code editor.
Also, install Postman or any API platform of your choice. You will use this to test the APIs you create.
Creating a Live HTTP Server
To start doing Deno things, let us first create a Deno server.
To create an HTTP server, you will need the http module from the standard module library.
Create a new file called app.ts in your project folder. In that file, import the `serve` method from the http module:
1import { serve } from 'https://deno.land/std@0.136.0/http/server.ts'
The serve method, when called, starts a server on port 8000 by default. This method also receives a callback function--a request handler--which allows you to respond to all requests made to the server. Here's how the method works:
1234567import { serve } from 'https://deno.land/std@0.136.0/http/server.ts' function requestHandler() { return new Response("Hey, I'm a server") } serve(requestHandler)
To run this file, you use the Deno CLI:
1deno run app.ts
You will get a warning when you run this command:
At the beginning of this article, I mentioned Deno being secure due to the permission policies it introduces to building applications.
By default, you cannot run a server on a port--using the computer's network resources. You have to add the flag --allow-net, which allows network access.
Note:: Refer to the Permissions list for more flags.
Now, run app.ts with the flag like this:
1deno run --allow-net app.ts
On `localhost:8000`, you can find the response of the server on Postman:
You can also specify a port like this:
1server(requestHandler, { port: 3000 })
With this code, the server will listen on port `3000`.
In your response, you can add options like body, status code, headers, etc. Here is an example:
12345678910function requestHandler() { const body = JSON.stringify({ message: 'Successfully created user' }) return new Response(body, { status: 201, headers: { 'content-type': 'application/json', }, }) }
One thing to note here is that this response is delivered to the client on every path (/, /users, /everything). What if you want to respond to requests on different paths? Let us look at creating API routes in Deno.
Create API Routes and Integrate Stream APIs
A typical backend server has APIs that clients can call to execute basic CRUD operations or to manipulate data in different ways.
Now, you will add two API routes to your backend server--one for getting a user and another for posting a user.
You can do this in two ways:
- Using the http module
- Using the abc module
Using the http module for API routes
The first is using the http module and analyzing every request URL and request method like this:
12345678910111213141516171819202122import { serve } from 'https://deno.land/std@0.136.0/http/server.ts' const PORT = 3000 function getUser(path: string): Response { const id = path.replace('/user/', '') // get the user id const user = id // get user from database const body = JSON.stringify({ user, message: 'User found' }) return new Response(body, { status: 200, headers: { 'content-type': 'application/json', }, }) } function requestHandler(req: Request): Response { const { url, method } = req const path = url.replace(
If you go to `localhost:3000`, you will get the default response, "Path does not exist" with a `400` status code:
But if you go to `localhost:3000/user/getstream`, you will get a different response because of the conditional statement where you inspected the URL and provided a different handler:
This code has a `getUser` method, a handler for a request with the `/user/` path, and the `'get'` method.
For other paths that are not handled, you use the default "Path does not exist" response with a `400` status code.
Using the abc module for API routes
Analyzing every URL like you did with the `http` module can make things harder to read, which brings us to the second method, the abc module from the Third Party Modules for Deno.
Note:: Third-Party Modules are Deno scripts created by other authors.
abc is similar to Express.js in Node.js. This module is a Deno framework that makes creating web applications easier. To create an API for getting users with abc, replace the content of app.ts with:
12345678910111213141516171819import { Application, Context } from 'https://deno.land/x/abc@v1.3.3/mod.ts' const app = new Application() const PORT = 3000 function getUser(ctx: Context) { const { id } = ctx.params const user = id // get user from database const body = JSON.stringify({ user, message: 'User found' }) return ctx.json(body, 200) } app.get('/user/:id', getUser) app.start({ port: PORT })
You will notice that this is more readable compared to the previous code. The `abc` module exposes an `Application` constructor, which you can use to add API routes and also start a server.
For a POST request that creates a user, add this to app.ts:
12345678async function createUser(ctx: Context) { const user = await ctx.body // add the user to database return ctx.json({ user, message: 'User created successfully' }, 200) } app.post('/user', createUser)
Here is the result after testing it on Postman:
When you create a user, where do you store them? Ideally, this would be a database, but for this article, I will show you how to store these users in a JSON file in the project. The aim of this method is to show you how to read and write files in the Filesystem using Deno.
Read/Write Files in the Filesystem
The global Deno object provides methods that you can use to read and write files. You will experiment with reading and writing on a users.json file. Create this file and paste the following array into it:
12345678910111213141516171819202122[ { "id": "mike", "name": "Mike", "image": "https://i.picsum.photos/id/92/200/200.jpg?hmac=2cxZLFe94hVFQL5AERTDzRKET_GDG-2qpFi5-ctPekg" }, { "id": "mary", "name": "Mary", "image": "https://i.picsum.photos/id/646/200/200.jpg?hmac=3jbia15y-hA5gmqVJjmk6BPJiisi4j-fNKPi3iXRiRo" }, { "id": "jake", "name": "Jake", "image": "https://i.picsum.photos/id/124/200/200.jpg?hmac=FuA4HgovVpaMlT_5gnjY_28jYCrrA2xrYXy3mJ9XDEw" }, { "id": "joe", "name": "Joe", "image": "https://i.picsum.photos/id/703/200/200.jpg?hmac=6zWxIBRmIf2e0jZTqvKBIwrc7wm-dPkvGky4go6Yyvg" } ]
This JSON file contains an array of user objects.
Let’s start by reading files.
Reading files with Deno
To read this file, use the `readTextFile` method in app.ts like this:
12const users = await Deno.readTextFile('./users.json') console.log(users)
Note:Deno currently supports top-level await so that you can use `await` outside an [`async` function](async function - JavaScript | MDN (mozilla.org)).
When you run this file with deno run --allow-net app.ts, you get this warning:
Again, you are attempting to do something with the device's resources, and you need permissions for that. The flag to allow this permission is --allow-read. Now, run this command:
1deno run --allow-next --allow-read app.ts
On your terminal, you'll see:
You should add this array, so the `getUser` method will work correctly in your API handler. Before doing that, you need to add an interface type for a user because you are using TypeScript. If you are using JavaScript, you can skip this step.
Create a new file interfaces/user.ts with the following:
12345export default interface User { id: string name: string image: string }
Next, update the `getUser` method in app.ts to this:
1234567891011121314151617import User from './interfaces/user.ts' // ... async function getUser(ctx: Context) { const { id } = ctx.params const users: User[] = JSON.parse(await Deno.readTextFile('./users.json')) const user = users.find((u) => u.id === id) if (user) { return ctx.json({ user, message: 'Success' }, 200) } else { return ctx.json({ message: 'User not found' }, 404) } }
When you run the Deno server, localhost:3000/user/joe produces this result:
And locahost:3000/user/aaa produces this:
Writing files with Deno
The global Deno object has the writeTextFile method for writing files. This method accepts two arguments: the file to write to and the text string of the file, respectively:
1Deno.writeTextFile(filepath, text)
If the file does not exist, the command will first create the file, then write the content to it.
Let us see the `writeTextFile` command in action. Add the following code to app.ts:
1Deno.writeTextFile('./test.txt', 'This is a test file')
Run the server with:
1deno run --allow-net --allow-read app.ts
You will get this warning:
As you may already expect, you will get another permission error while writing a file. The flag to permit this is --allow-write. Run the previous command with the flag like this:
1deno run --allow-net --allow-write --allow-read app.ts
You will see a test.txt file in your folder with the "This is a test file" text.
Next, use this method for your createUser API handler. Update the method to this:
1234567891011121314151617181920212223async function createUser(ctx: Context) { const { id, name, image } = (await ctx.body) as User const newUser: User = { id, name, image, } const users: User[] = JSON.parse(await Deno.readTextFile('./users.json')) const userExists = users.find((u) => u.id === newUser.id) if (userExists) { return ctx.json({ message: 'User with id already exists' }, 400) } const newUsers = [...users, newUser] Deno.writeTextFile('./users.json', JSON.stringify(newUsers)) return ctx.json({ message: 'Success', user: newUser }, 200) }
By testing with Postman:
You will have the new user in the users.json file on your device.
Manage Environment Variables
Server-side applications usually contain environment variables, which are secret credentials that are not included in the source code but are exposed during runtime.
To store an environment variable in Deno, you can create a .env file and store it like you usually would in other frameworks:
API_KEY=my-api-key
To read environment variables from this env file, you need to use the dotenv third-party module.
To use it, go to app.ts and paste the following:
12345import { config } from 'https://deno.land/x/dotenv/mod.ts' // ... const envs = config() console.log(envs)
Since this will read from a file, you need to pass the --allow-read flag. Run the following command:
1deno run --allow-net allow-read allow-write app.ts
You will get this in your terminal:
Note: You can pass the --allow-all flag to allow all permissions. But this is not recommended, so as Deno said, use with caution.
Generating JWT Tokens
JSON Web Tokens (JWT) is a popular method for authentication in applications. The usual flow to create a JWT (or “token”) involves:
- Creating a token on a backend server
- Sending the token to a client, which saves the token
- Using that token in subsequent authentication-required requests
JWTsJWTs are broken down into three parts: the header, the payload, and the signature. The signature is generated using the header, the payload, and the key.
To generate a JWT on Deno, use the djwt third-party module.
To create a token with this module, here is the syntax:
1234567import { create, Header, Payload } from 'https://deno.land/x/djwt@v2.4/mod.ts' create( header: Header, payload: Payload, key: CryptoKey | null )
Note: The key for the `create` method is not a string. It has to be of type `CryptoKey`, which I will show you how to create in a second.
Now, create a JWT. Start by creating a new file called token.ts with the following code:
123456789import { create, Payload, getNumericDate, Header, } from 'https://deno.land/x/djwt@v2.4/mod.ts' import { config } from 'https://deno.land/x/dotenv@v3.2.0/mod.ts' const { API_KEY } = config()
For the JWT, you will use the `API_KEY` in your environment variables to create the signature as that is unique.
Add the following:
1234567891011121314151617181920212223242526const encoder = new TextEncoder() const keyBuf = encoder.encode(SECRET_KEY) const key = await crypto.subtle.importKey( 'raw', keyBuf, { name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify'] ) const payload: Payload = { iss: 'deno-demo', exp: getNumericDate(300), // expires in 5 min. } const header: Header = { alg: 'HS256', typ: 'JWT', } export const generateToken = async () => { const token = await create(header, payload, key) return token; }
Because the `API_KEY` is a string, you have to convert it to a `CryptoKey`. To do this, you use the SubtleCrypto.importKey() method.
This method accepts the following parameters:
- The data format of the key
- The key data (of the type `TypedArray`)
- The algorithm
- The extractable boolean value
- The usage of the keys (in your case, the key will be used to sign and verify the token.)
Next, call the `generateToken` method. Go to app.ts, and add the following:
1234import { generateToken } from './token.ts' const token = await generateToken() console.log(token)
By running the server, you will see the following information in your terminal:
Bringing It All Together
In the previous sections, we have covered how to achieve basic server-side concepts with Deno. Now, let us bring it all together to create a server-side application.
Create three new folders in the root directory:
- utils
- demo
- controllers
Now, you should have four folders: utils, demo, controllers, and interfaces.
Move the `token.ts` file to the utils directory. Move the `users.json` file to the demo directory.
In the controllers directory, create a new file called users.ts. In the file, add the `getUser` and `createUser` methods:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import { Context } from 'https://deno.land/x/abc@v1.3.3/mod.ts' import User from '../interfaces/user.ts' import { generateToken } from '../utils/token.ts' export async function getUser(ctx: Context) { const { id } = ctx.params const users: User[] = JSON.parse( await Deno.readTextFile('../demo/users.json') ) const user = users.find((u) => u.id === id) if (user) { return ctx.json({ user, message: 'Success' }, 200) } else { return ctx.json({ message: 'User not found' }, 404) } } export async function createUser(ctx: Context) { const { id, name, image } = (await ctx.body) as User const token = await generateToken() const newUser: User = { id, name, image, } const users: User[] = JSON.parse( await Deno.readTextFile('../demo/users.json') ) const userExists = users.find((u) => u.id === newUser.id) if (userExists) { return ctx.json({ message: 'User with id already exists' }, 400) } const newUsers = [...users, newUser] Deno.writeTextFile('./users.json', JSON.stringify(newUsers)) return ctx.json({ message: 'Success', user: newUser, token }, 200) }
In the `createUser` method in the code above, you generate a token and send it to the client in the response.
Lastly, the app.ts file. Replace the content with:
1234567891011import { Application } from 'https://deno.land/x/abc@v1.3.3/mod.ts' import { getUser, createUser } from './controllers/users.ts' const app = new Application() const PORT = 3000 app.get('/user/:id', getUser) app.post('/user', createUser) app.start({ port: PORT })
And there you have your first Deno application, which incorporates API routes, a token generation feature (with environment variables), the Deno Filesystem as a database, and a listener on a PORT for requests.
Conclusion
The features mentioned here are not all you can do with Deno. There's more, just like an ideal server-side application will support.
We have gone through the basic concepts to set you up for your Deno journey. As an improvement to this application, you can connect to a database, implement GraphQL instead of Rest APIs, etc.
The Deno Documenation contains more information, so check it out to learn more. Also, you can explore many other standard and third-party modules. There are so many of them for so many needs you may have.
If you found this helpful, kindly share.