How to Deploy a Deno Application?

How to Deploy a Deno Application?
How to Deploy a Deno Application_

Various deployment options exist for web applications built with Deno. However, containerization as a service platform has become a popular choice in recent times due to the various advantages they offer over other deployment options.

In this article, you will explore Deno, its advantages, and its limitations. Additionally, you will build a simple Deno app and deploy it on Back4app containers.

What is Deno?

Deno is a secure and modern runtime for JavaScript and TypeScript created to address limitations and design flaws found in Node.js.

Unlike Node.js, It emphasizes security by default, enforcing granular permissions for file system and network access.

Additionally, Deno natively supports TypeScript, eliminating the need for additional setup or transpilation steps, among other features.

Since its release in 2018, Deno has garnered attention and interest from developers due to its improvements over Node.js.

However, while Deno offers improvements, Node.js remains a mature ecosystem with extensive community support and a vast package repository.

Notwithstanding, Deno has attracted a growing community of developers who appreciate its approach and are exploring its potential.

Advantages of Deno

Deno’s rise to popularity is due to some underlying reasons. Some are as follows.

Improved Security over Node.js

Deno offers improved security as a key advantage, implementing a permission-based security model and running applications in a sandboxed environment.

It implements a permission-based security model, where explicit authorization is required for accessing resources like the file system and network.

By default, Deno operates in a restricted mode and runs applications in a sandboxed environment, limiting potentially risky actions, isolating them from the underlying system, and preventing direct access to sensitive resources.

Comprehensive security audits and meticulous code reviews further reinforce Deno’s robust security. These measures provide you with a dependable and secure platform for building applications, instilling confidence in their security, and protecting against potential vulnerabilities.

Dependency Management

Deno offers a distinct approach to dependency management compared to traditional JavaScript runtime environments like Node.js.

Instead of relying on a centralized package registry, Deno utilizes URLs for importing modules directly from the web.

This approach simplifies the process by eliminating the need for a separate package manager like npm and mitigating concerns related to version conflicts and the complexities of managing the “node_modules” folder.

You can specify dependencies by specifying the URLs of the modules they want to import, making sharing and distributing code easier. This decentralized approach to dependency management in Deno promotes simplicity, reduces friction, and helps ensure a more streamlined development experience.

TypeScript Support Out of the Box

Deno offers native and seamless support for TypeScript, making it an excellent choice if you prefer or require TypeScript in your projects.

TypeScript is a typed superset of JavaScript that brings static typing and other advanced language features to JavaScript development. With Deno, there is no need for additional configuration or build steps to use TypeScript.

Deno comes bundled with the TypeScript compiler, allowing you to write and run TypeScript code directly.

This native support eliminates the complexities of setting up a separate TypeScript toolchain and simplifies the development process.

It enables you to take advantage of TypeScript’s type checking, improved tooling, and enhanced developer experience while building applications with Deno.

Limitations of Deno

However, Deno has certain limitations which are affecting its adoption. Some of them are as follows.

Immature Ecosystem

One of the limitations of Deno is the maturity of its ecosystem. Compared to Node.js, which has been around for a longer time, Deno’s ecosystem is still relatively new and evolving.

This means there may be fewer third-party libraries, frameworks, and tools specifically designed for Deno. You may need to build certain functionalities from scratch or adapt existing Node.js packages for use in Deno.

The smaller community size also means that there may be fewer resources, tutorials, and community support available compared to the well-established Node.js ecosystem.

However, as Deno gains popularity and adoption, its ecosystem is expected to grow and mature, offering a broader range of libraries and tools in the future.

Steep Learning Curve

Another limitation of Deno is the learning curve associated with transitioning from other JavaScript runtime environments, such as Node.js.

Deno introduces new concepts, APIs, and patterns that you may need to familiarize yourself with. This includes understanding Deno’s module system, permission-based security model, and the differences in how certain functionalities are implemented compared to other runtimes.

Developers who are already proficient in Node.js may need to invest time and effort into learning and adapting to the specific features and conventions of Deno.

However, The learning curve can be managed by referring to the official Deno documentation, engaging with the Deno community, and exploring available learning resources.

Compatibility with Node.js Libraries

Compatibility with Node.js libraries is another limitation of Deno. Due to differences in the module systems and runtime environments, not all Node.js libraries and modules can be directly used in Deno without modifications.

Deno uses ES modules (ECMAScript modules) as its module system, while Node.js traditionally uses CommonJS modules. This difference in module formats can lead to incompatibilities when importing and using Node.js-specific modules in Deno.

Developers may need to make adjustments or find alternative libraries that are specifically designed to work with Deno’s module system.

While Deno provides a compatibility layer to run some Node.js modules, it may not cover all cases, and manual modifications or adaptations might be required.

Deno Deployment Options

There are several deployment options for Deno apps, and some of them include the following.

Infrastructure-as-a-Service (IaaS)

Infrastructure-as-a-Service (IaaS) is a cloud computing model that offers virtualized computing resources. With IaaS, you can rent virtual machines, storage, and networking from cloud providers on a pay-as-you-go basis. This allows you to set up and manage your own virtualized infrastructure without investing in physical hardware.

IaaS options enable you to run Deno applications on virtual machines. Popular IaaS platforms like AWS, Google Cloud, and Microsoft Azure offer flexible and scalable solutions, allowing you to configure the infrastructure according to your application’s specific needs.

However, while IaaS grants you greater control and resource isolation, it also demands more manual setup and management, involving tasks such as server provisioning, security updates, and monitoring.

Therefore, IaaS is a viable choice when you require extensive control over your infrastructure and have the expertise to handle its complexities effectively.

Container-as-a-Service (CaaS)

Container-as-a-Service (CaaS) is a cloud computing model that simplifies the deployment and management of containerized applications.

With CaaS, you can focus on building and deploying applications without worrying about the underlying infrastructure.

Deno applications can be deployed within containers, ensuring consistency and isolation. Back4app containers is a popular CaaS option for Deno deployment.

CaaS platforms offer scalability and resource isolation, with each application running in its own container, enhancing security and stability.

The containers’ consistency ensures that Deno applications can be easily deployed on any platform that supports containers.

While CaaS solutions have a learning curve, they provide significant benefits for applications requiring dynamic scaling and deployment across multiple nodes or clusters.

Deno Installation Process

Before you can use Deno, you have to download and install it. Deno’s installation varies depending on your operating system.

On macOS and Linux, you can install Deno by running the command below:

curl -fsSL <https://deno.land/x/install/install.sh> | sh

On Windows, you can install Deno using Powershell by running the command below:

irm <https://deno.land/install.ps1> | iex

To confirm that your installation was successful, you can run the command below, and it should print a version number to your terminal.

deno --version

If you don’t see the version number, try installing Deno again.

Setting up a Deno Project

To create a simple API with Deno, you will need a router, a server, and a database.

Before following the steps below, create an src folder in your project’s root directory. This folder will contain all your project’s source files.

Step 1: Creating a Dependency File

Unlike Node.js, Deno does use package managers like NPM or Yarn. Rather, the packages are imported directly from their URL.

To mimic the functions of a package.json file, create a deps.ts in your project’s root directory and add the code block below to it.

export { Application, Router } from "https://deno.land/x/[email protected]/mod.ts";
export type { RouterContext} from "https://deno.land/x/[email protected]/mod.ts";
export { config as dotenvConfig } from "https://deno.land/x/[email protected]/mod.ts";
export { Client } from "https://deno.land/x/[email protected]/mod.ts";

The code block above imports (Installs) and exports Application, Router, and RouterContex from Oak. config from dotenv and Client from deno-postgres.

Step 2: Creating a Server

For this step, you will create a simple HTTP server with Oak. Oak is a middleware for Deno’s HTTP server based on Koa.js, a framework for Node.js similar to Express but more lightweight.

To create an HTTP server with Oak, create a server.ts file in your src and add the code block below to it.

import { Application } from "../deps.ts";
import config from "../config/default.ts";

import router from "./router.ts";

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: config.port });

The code block above creates an HTTP server with Oak and registers a router to handle all the incoming traffic.

The app.use(router.routes()) line registers the router’s routes as middleware in the Oak application. All incoming requests will be matched against the registered routes, and the corresponding handlers will be executed if a match is found.

If a match is not found, the app.use(router.allowedMethods()) line handles them by sending appropriate responses such as a 404 not found or a 405 not allowed.

Step 3: Managing Environmental Variables

Storing sensitive data, such as API Keys, Database credentials, etc, in plain text poses a security risk. Anyone that gets hold of your keys or credentials may have unrestricted access to your application. This can result in data loss and data theft, among other possible exploits.

It is considered good practice to store sensitive data in environmental variables to avoid situations like this.

Create a .env file in your project’s root folder and store your database credentials and other sensitive information in the file.

Like so:

#.env
DB_URI = <YOUR_POSTGRES_DB_URI>
PORT = 8000

Replace <YOUR_POSTGRES_DB_URI> with your database credentials.

Next, create a config folder in your project’s root folder, and in the config folder, create a default.ts file and add the code block below to it.

//default.ts
import { dotenvConfig } from "../deps.ts";

dotenvConfig({
  export: true,
  path: "../.env",
});

const config = {
  db: {
    dbUri: Deno.env.get("DB_URI"),
  },
  port: 3000
};

export default config;

The code block above securely retrieves the values stored in your .env file and exposes it to the rest of your application.

Step 3: Connecting to a Database

For this step, you will connect your application to a Postgres database. You will need the database to store and retrieve data for your application.

Create a db.ts file in your src folder and add the code block below to it.

//db.ts
import { Client } from "../deps.ts";
import config from "../config/default.ts";

let postgresConfiguration = config.db.dbUri;

const client = new Client(postgresConfiguration);

await client.connect();

export default client;

The code block above attempts to connect your application to a Postgres database using the URI you provided in your .env file.

Step 4: Creating a Database Repository

Create a blogRepository file in your src folder and add the code below to your file.

//blogRepository.ts
import client from "./db.ts";

class BlogRepository {
  async createBlogTable() {
    const blog = await client.queryArray(
      `CREATE TABLE IF NOT EXISTS blogs (id SERIAL PRIMARY KEY, title VARCHAR(255), body VARCHAR(255), author VARCHAR(255))`
    );

    return blog;
  }

  async getAllBlogs() {
    const allBlogs = await client.queryArray("SELECT * FROM blogs");

    return allBlogs;
  }

  async getBlogById(id: string) {
    const blog = await client.queryArray(
      `SELECT * FROM blogs WHERE id = ${id}`
    );

    return blog;
  }

  async createBlog(title: string, body: string, author: string) {
    const blog = await client.queryArray(
      `INSERT INTO blogs (title, body, author) VALUES ('${title}', '${body}', '${author}')`
    );

    return blog;
  }

  async updateBlog(id: string, title: string, body: string, author: string) {
    const blog = await client.queryArray(
      `UPDATE blogs SET title = '${title}', body = '${body}', author = '${author}' WHERE id = ${id}`
    );

    return blog;
  }

  async deleteBlog(id: string) {
    const blog = await client.queryArray(`DELETE FROM blogs WHERE id = ${id}`);

    return blog;
  }
}

export default new BlogRepository();

The code block above will handle all the database operations by abstracting raw SQL queries and exposing simple methods that you can use to interact with your Postgres database.

Step 5: Creating Route Handlers

For this step, you will create route handlers to handle simple CRUD functions for your application. The supported routes are as follows:

  • GET /api/blogs: Returns all the blogs in your database
  • GET /api/blog/:id: Returns a single blog with the matching id provided in the URL params.
  • POST /api/blog/new: Creates a new blog in your database.
  • PUT /api/blog/:id: Updates a blog with the matching id provided in the URL params.
  • DELETE /api/blog/:id: Deletes a blog with the matching id provided in the URL params.

Create a router.ts file in your src folder and add the following imports to it.

import { Router, RouterContext } from "../deps.ts";
import blogRepository from "./blogRepository.ts";

Next, create a router instance by adding the code block below to it:

const router = new Router();

To register your router handlers, you have to chain them to the router instance.

For example (GET /api/blogs):

router
  .get("/api/blogs", async (ctx: RouterContext<"/api/blogs">) => {
    const data = await blogRepository.getAllBlogs();
    console.log(data);

    //format data
    const allBlogs = data.rows.map((blog) => {
      return {
        id: blog[0],
        title: blog[1],
        body: blog[2],
        author: blog[3]
      };
    });

    ctx.response.body = allBlogs;
  })

The code block above create a route handler for GET /api/blogs by chaining the handler logic to the router instance.

Chain them to the previously chained method to register the rest of the routes. Like so:

GET /api/blog/:id:

.get("/api/blog/:id", async (ctx: RouterContext<"/api/blog/:id">) => {
    try {
      const data = await blogRepository.getBlogById(ctx.params.id);
      console.log(data);

      //format data
      const blog = data.rows.map((blog) => {
        return {
          id: blog[0],
          title: blog[1],
          body: blog[2],
          author: blog[3]
        };
      });

      ctx.response.body = blog;
    } catch (error) {
      ctx.response.status = 500;
      ctx.response.body = {
        msg: "Error getting blog",
        error,
      };
    }
  })

POST /api/blog/new

.post("/api/blog/new", async (ctx: RouterContext<"/api/blog/new">) => {
    const resBody = ctx.request.body();
    const blog = await resBody.value;

    if (!blog) {
      ctx.response.status = 400;
      ctx.response.body = { msg: "Invalid data. Please provide a valid blog." };
      return;
    }

    const { title, body, author } = blog;

    if (!(title && body && author)) {
      ctx.response.status = 400;
      ctx.response.body = {
        msg: "Title or description missing. Please provide a valid blog.",
      };
      return;
    }

    try {
      await blogRepository.createBlog(title, body, author);

      ctx.response.status = 201;
      ctx.response.body = {
        msg: "blog added successfully",
      };
    } catch (error) {
      ctx.response.status = 500;
      ctx.response.body = {
        msg: "Error adding blog",
        error,
      };
    }
  })

PUT /api/blog/:id:

.put("/api/blog/:id", async (ctx: RouterContext<"/api/blog/:id">) => {
    try {
      const resBody = ctx.request.body();
      const blog = await resBody.value;

      if (!blog) {
        ctx.response.status = 400;
        ctx.response.body = {
          msg: "Invalid data. Please provide a valid blog.",
        };
        return;
      }

      const { title, body, author } = blog;

      if (!(title && body && author)) {
        ctx.response.status = 400;
        ctx.response.body = {
          msg: "Title or description missing. Please provide a valid blog.",
        };

        return;
      }

      await blogRepository.updateBlog(ctx.params.id, title, body, author);

      ctx.response.status = 200;
      ctx.response.body = {
        msg: "blog updated successfully",
      };
    } catch (error) {
      console.log(error);
      ctx.response.status = 500;
      ctx.response.body = {
        msg: "Error updating blog",
        error: error.message,
      };
    }
  })

DELETE /api/blog/:id:

.delete("/api/blog/:id", async (ctx: RouterContext<"/api/blog/:id">) => {
    await blogRepository.deleteBlog(ctx.params.id);

    ctx.response.status = 200;
    ctx.response.body = {
      msg: "blog deleted successfully",
    };
  });

Then, export your router instance. Like so:

export default router;

Finally, modify your server.ts file to create a blog database when you application starts for the first time.

Like so:

import { Application } from "../deps.ts";
import config from "../config/default.ts";
import blogRepository from "./blogRepository.ts";

import router from "./router.ts";

const app = new Application();

(async () => {
  await blogRepository.createBlogTable();
})();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: config.port });

The modified code adds an IIFE that creates a new blog table in your database to the server.ts file.

Deploying Your Deno App on Back4app Containers

To deploy your Deno app on Back4app containers, you need the follow the steps below:

Step 1: Create a Dockerfile

A Dockerfile provides specific instructions for building a Docker image. These instructions guide the process of building the image.

Run the command below to create a Dockerfile:

touch Dockerfile

The command above creates a Dockerfile in your project’s root directory.

Next, add the code block below to your Dockerfile:

FROM denoland/deno:latest

EXPOSE 8000

WORKDIR /app

COPY deps.ts .
RUN deno cache deps.ts

COPY . .

RUN deno cache src/server.ts

CMD ["run", "--allow-net", "--allow-env", "--allow-read", "src/server.ts"]

The Dockerfile above sets up a containerized environment for running a Deno application. It caches the application dependencies and entry point, and then runs the Deno application with the specified permissions when the container is started.

The application is expected to listen on port 8000, though the actual port mapping must be done when running the container.

Finally, push your code to GitHub.

Step 2: Create a new Back4app Application

To create a Back4app application, visit the official Back4app website. Once there, locate the Sign up button at the top-right corner of the Back4app landing page. You’ll be directed to a registration form upon clicking the Sign up button. Proceed to fill out this form with the required details, such as your email address, username, and password. Make sure to provide accurate information. After completing the form, submit it.

If you already have an account, click on Log in instead.

Once you’ve successfully set up your Back4app account, log in to access your account dashboard. From there, locate the “NEW APP” button and click on it.

This action will direct you to a page where you’ll be presented with different options for creating your new app. Since your intention is to deploy using containerization, opt for the “Containers as a Service” option.

Next, connect your GitHub account to your Back4app account. You can give Back4app access to all the repositories in your account or specific repositories.

Choose the application you want to deploy, in this case, the application you built in this tutorial, and click Select.

Choose repository to deploy

Clicking the select button will take you to a page where you will be required to fill out some information about your app, such as the name, branch, root directory, auto-deploy options, port, health, and environmental variables.

Ensure you provide all the necessary environmental variables your application requires to operate correctly. Once you have completed filling out the required information, proceed to click on the ‘Create App’ button.

Fill your app details

This will start the deployment process, and after a while, your deployment should be ready. If your deployment process is taking a lot of time, you can check the logs to see if an error occurred with the deployment or check Back4app’s troubleshooting guide.

Conclusion

In this article, you explored Deno, its advantages, limitations, its popular deployment options, how to build an app with Deno, and deploy your app using Back4app containers.

Despite its limitations, due to its security model, Deno can be ideal for building very secure and sensitive applications. Additionally, its native TypeScript support removes takes away the troubles of setting up TypeScript in your project.

By following the steps outlined in this article you can easily create and deploy your Deno application on Back4app containers.

FAQ

What is Deno?

Deno is a secure and modern JavaScript/TypeScript runtime that allows developers to build server-side and client-side applications.

How to Deploy a Deno Application?

1. Create a Deno app
2. Create a Dockerfile
3. Push your Deno app to GitHub and connect your GitHub account to your Back4app account.
4. Create a CaaS app on Back4app
5. Select your Deno app from your list of repositories
6. Deploy your Deno app


Leave a reply

Your email address will not be published.