Opine Tutorial Part 1: Express for Deno
Tags: deno, typescript, webdev, tutorial
Published: 2020-05-26 11:58:32

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](https://deno.land/) web framework ported from [Express](https://expressjs.com/), 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](https://github.com/asos-craigmorten/opine/blob/main/.github/CHANGELOG.md) for information about changes in the current release and [GitHub](https://github.com/asos-craigmorten/opine/) 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](https://github.com/oakserver/oak) which is based on Koa, [Pogo](https://github.com/sholladay/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](https://github.com/asos-craigmorten/opine/tree/main/examples/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`

```ts
import opine from "https://deno.land/x/opine@0.21.2/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 `/`.

```ts
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.

```ts
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:

```ts
// 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](https://github.com/asos-craigmorten/opine/tree/main/examples/downloads) 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](https://github.com/pillarjs/path-to-regexp) 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:

```ts
// blog.ts - Posts route module

import { Router } from "https://deno.land/x/opine@0.21.2/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/`.

```ts
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:

```ts
import opine from "https://deno.land/x/opine@0.21.2/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:

```ts
import { opine, serveStatic } from "https://deno.land/x/opine@0.21.2/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:

```txt
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.

```ts
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":

```ts
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](https://github.com/asos-craigmorten/opine/blob/main/.github/API/middlewares.md#servestaticroot-options) 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.

```ts
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](https://github.com/asos-craigmorten/opine).

---

_Inspiration:_ This article draws heavy influence from the [Express web framework](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/development_environment) series. Credit for style, structure and key points should be attributed to the [original author(s) at MDN](https://wiki.developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/development_environment$history).