Skip to main content

Opine Tutorial Part 1: Express for Deno

In this first Opine article we answer the question "What is Opine?".

We'll outline the main features, and show you some of the main building blocks of an Opine application.

Prerequisites: A general understanding of server-side website programming, and in particular the mechanics of client-server interactions in websites. Familiarity with Deno. A basic familiarity with Express is a good to have, but not required.

Objective: To gain familiarity with what Opine is and how it fits in with Deno, what functionality it provides, and the main building blocks of an Opine application.


Introducing Opine

Opine is a new Deno web framework ported from Express, which is one of the most popular Node web frameworks. It provides mechanisms to:

  • Write handlers for requests with different HTTP methods at different URL paths.
  • Setup common settings like the port to use for connecting.
  • Add additional request processing "middleware".

While Opine itself is fairly minimalist, there is already a vast ecosystem of middleware packages that have been built for Express on Node to address almost any web development problem. If an existing Express middleware is Deno compatible, or has a ported version for Deno, then it will work with Opine.

Where did Opine come from?

Opine was initially released very recently in May 2020 and is currently on version 0.4.0. You can check out the changelog for information about changes in the current release and GitHub for further documentation and information.

Opine was created to create an Express-like offering for Deno. There are several web frameworks already available for Deno such as Oak which is based on Koa, Pogo which based on Hapi, and many more - but few currently offer an Express-like API with the same depth of features and documentation.

Opine aims to achieve the same great goals of Express, focusing first on developing robust tooling and features before moving onto accelerating performance and becoming super lightweight. As time passes, Opine's goals may naturally diverge from Express.

Is Opine opinionated?

Much like Express, Opine is currently an unopinionated web framework (contrary to it's name!). You can insert any compatible middleware in any order you like into the request handling chain. You can also structure your app in any way that suits you and your needs.

What does Opine code look like?

Opine provides methods to specify what function is executed for any requested HTTP method (GET, POST, SET, etc.) and URL path patterns. You can use Opine middleware to add support for serving static files, getting POST/GET parameters, etc. You can use any database mechanism supported by Deno (Opine does not define any database-related behaviour).

The following sections explain some of the common things you'll see when working with Opine and Deno code.

Hello Deno!

First lets consider the standard Opine Hello World example (we discuss each part of this below, and in the following sections).

Tip: If you have Deno already installed, you can save this code in a text file called app.ts and run it in a bash command prompt by calling:

deno run --allow-net ./app.ts

import opine from "https://deno.land/x/opine@main/mod.ts";
const app = opine();

app.get("/", function (req, res) {
  res.send("Hello Deno!");
});

app.listen(3000);
console.log("Opine started on port 3000");

The first two lines import the Opine module and create an Opine application. This object, which is traditionally named app, contains all the methods for routing requests, configuring middleware, and modifying application settings.

The app.get block of code is an example of a route definition. The app.get() method specifies a callback function that will be invoked whenever there is an HTTP GET request to the path / relative to the site root. The callback function takes a request and a response object as arguments, and simply calls send() on the response to return the string "Hello Deno!".

The final block of code starts up the server on port 3000 and prints a log comment to the console. If you were to run the server, you could open http://localhost:3000/ to see the example response.

Creating route handlers

In our "Hello World" Opine example (see above), we defined a route handler function for HTTP GET requests to the site root /.

app.get("/", function (req, res) {
  res.send("Hello Deno!");
});

The route handler function takes a request and a response object as arguments. In this case, the method simply calls send() on the response to return the string "Hello Deno!". There are a number of other response methods that can be used for ending the request, for example, you can call res.json() to send a JSON response or res.sendFile() to send a file on a particular path.

Tip: You can use any name you like for the route handler but traditionally req and res are used. You can also use object destructing to just get the property or method of the req or res that you need.

The Opine application object also provides methods to define route handlers for the other HTTP methods. These can be used in a similar way to the example above.

checkout(), copy(), delete(), get(), head(), lock(), merge(), mkactivity(), mkcol(), move(), m-search(), notify(), options(), patch(), post(), purge(), put(), report(), search(), subscribe(), trace(), unlock(), unsubscribe()

There is also an additional routing method app.all() which will be executed for any HTTP method. This is can be useful for loading middleware functions at a particular path for all requests regardless of the method used. An example use-case may be for authenticated to ensure all incoming requests are authenticated regardless of HTTP method, and if not, respond with a 401 (Unauthorized) response.

app.all("/secret", function(req, res, next) {
  console.log("Accessing the secret section ...");

  // Perform authentication checks

  if (!authenticated) {
    // Respond with a 401 (Unauthorized) response
    res.sendStatus(401);
  } else {
    // Pass control to the next handler in the chain
    next(); 
  }
});

Routes also allow you to match particular patterns of characters in a URL, and extract some values from the URL so they can be used as parameters in the route handler:

// If we used `/files/*`, the name could be accessed via req.params[0]
// but here we have named it using :file
app.get("/files/:file(*)", async function (req, res, next) {
  const filePath = join(__dirname, "files", req.params.file);

  try {
    await res.download(filePath);
  } catch (err) {
    // File for download not found
    if (err instanceof Deno.errors.NotFound) {
      res.status = 404;
      res.send("Can't find that file, sorry!");
    } else {
      // Non-404 error
      next(err);
    }
  }
});

The above snippet is taken from the downloads example in the Opine GitHub repository. It uses a wildcard path to extract the file from the requested URL and this is then made available to the request handler function in the req.params.file property. To find out more about path pattern matching check out the path-to-regex GitHub repository.

Often it can be useful to group route handlers for a particular part of a site together and access them using a common route-prefix. For example, a blogging website might have all blog-related routes in one file, and have them accessed with a route prefix of /blog/. In Opine this is achieved by using the Router object.

For example, we can create our posts route in a module named blog.ts, and then export the Router object, as shown below:

// blog.ts - Posts route module

import { Router } from "https://deno.land/x/opine@main/mod.ts";
const router = Router();

// Home page route
router.get("/", function(req, res) {
  res.send("Blog home page.");
});

// About page route
router.get("/about", function(req, res) {
  res.send("About this blog.");
});

export default router;

Note: Adding routes to the Router object is just like adding routes to the app object as we have done previously.

To use the blog router in our main app file we would then import the route module (blog.ts), then call use() on the Opine application to add the Router to the middleware handling path. The two routes will then be accessible from /blog/ and /blog/about/.

import blog from "./blog.ts";
// ...
app.use("/blog", blog);

Using middleware

Middleware is used extensively in Opine apps, for tasks from serving static files to error handling.

Whereas the route functions we've seen above tend to end the HTTP request by returning a response, middleware functions tend to perform an operation on the request or response objects, and then call the next function in the "stack", which could be more middleware or a route handler.

Note: Middleware can perform any operation, make changes to the request and response objects, and end the request. If it does not end the cycle then it must call next() to pass control to the next middleware function otherwise the request will be left hanging!

To use middleware, import them into your app file and then call the use() method on the Opine object to add the middleware to the stack:

import opine from "https://deno.land/x/opine@main/mod.ts";
import myMiddleware from "./myMiddleware.ts";

const app = opine();

const myOtherMiddleware = function(req, res, next) {
  // ... Perform some operations
  next();
};

// Function added with use() for all routes and verbs
app.use(myMiddleware);

// Function added with use() for a specific route
app.use("/someroute", myOtherMiddleware);

// A middleware function added for a specific HTTP method and route
app.get("/", myOtherMiddleware);

app.listen({ port: 3000 });

Note: Middleware and routing functions are called in the order that they are declared. Make sure to take care if the order matters to any of your middleware.

The only difference between middleware and route handlers is that middleware functions have a third argument next, which middleware functions should call if they do not end the request.

Serving static files

You can use Opine's serveStatic middleware to serve static files such as images, CSS and JavaScript. For example, you would use the code below to serve static images, CSS files, and JavaScript files from a directory named "public" in the current working directory of your application where you executed the deno run command:

import { opine, serveStatic } from "https://deno.land/x/opine@main/mod.ts";
const app = opine();

app.use(serveStatic("public"));

Every file in the "public" directory are served by adding their filename (relative to the base "public" directory) to the base URL. So for example:

http://localhost:3000/images/dog.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/about.html

You can call serveStatic() multiple times to serve multiple directories. If a file cannot be found by one middleware function then it will simply be passed on to the subsequent middleware.

app.use(serveStatic("public"));
app.use(serveStatic("media"));

You can also create a prefix path for your static URLs. For example, here we specify a mount path so that the files are loaded with the prefix "/media":

app.use("/media", serveStatic("public"));

Now, you can load the files that are in the public directory from the /media path prefix.

For more information about serving static files see the Middlewares Opine documentation.

Handling errors

Errors are handled by one or more special middleware functions that have four arguments, instead of the usual three: (err, req, res, next). This might feel familiar as it is exactly the same as Express.

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.setStatus(500).send('Something broke!');
});

These can return any content required, but should be called after all other app.use() and routes calls so that they are the last middleware to handle requests.

Opine comes with a built-in error handler, which takes care of any remaining uncaught errors that might be encountered in the app. This default error-handling middleware function is added at the end of the middleware function stack, so if you pass an error to next() and you do not handle it yourself in an error handler, it will be handled by the built-in error handler.

File structure

Opine makes no assumptions in terms of structure or what components you use, exactly the same as Express. Routes, views, static files, and other application-specific logic can live in any number of files with any directory structure.

While it is perfectly possible to have a whole Opine application in one file, typically it makes sense to split your application into files based on function or architectural domain.

Summary

Congrats! You've made it through the first runthrough of Opine for Deno and should now understand what the main parts of an Opine app might look like (routes, middleware and error handling).

Opine is deliberately a very lightweight web application framework, and very similar Express, so much of it's benefit and potential comes from familiarity with the Express API and the potential to port over third party libraries and features from Node to Deno and be able to use them with a web framework straight away.

We'll look at doing that in more detail in the future articles. In the next article we'll start working through a tutorial to build a complete web application using the Deno environment and the Opine web framework.

Questions, comments and feedback very welcome! Drop messages below or add issues to the Opine GitHub.

Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...