DEV Community

Cover image for Opine Tutorial Part 1: Express for Deno
Craig Morten
Craig Morten

Posted on • Edited on

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@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");
Enter fullscreen mode Exit fullscreen mode

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!");
});
Enter fullscreen mode Exit fullscreen mode

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(); 
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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@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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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@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 });
Enter fullscreen mode Exit fullscreen mode

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@0.21.2/mod.ts";
const app = opine();

app.use(serveStatic("public"));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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!');
});
Enter fullscreen mode Exit fullscreen mode

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.


Inspiration: This article draws heavy influence from the Express web framework series. Credit for style, structure and key points should be attributed to the original author(s) at MDN.

Top comments (18)

Collapse
 
jdelarubia profile image
jdelarubia

Hi Craig,
At the beginning, the line
import opine from "https://deno.land/x/opine@master/mod.ts";
should read:
import opine from "https://deno.land/x/opine@main/mod.ts"; ?
Just changing @master for @main.

Collapse
 
craigmorten profile image
Craig Morten • Edited

Yes you are correct - thanks for pointing it out, Will update 😄

Update: all changed - thanks!

Collapse
 
iampeters profile image
Peters Chikezie • Edited

this is awesome. looks very easy to use. thanks

Collapse
 
iampeters profile image
Peters Chikezie

I tried to use your example to get it to work with my code but I got this error

NotFound: No such file or directory (os error 2)
at unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)
at Object.sendSync ($deno$/ops/dispatch_json.ts:69:10)
at Object.openSync ($deno$/ops/fs/open.ts:22:10)
at Object.openSync ($deno$/files.ts:28:15)
at Object.readFileSync ($deno$/read_file.ts:6:16)
at readFileStrSync (read_file_str.ts:18:30)
at parseApiFile (parseApiFile.ts:13:23)
at getSpecificationObject (getSpecificationObject.ts:16:24)
at swaggerDoc (index.ts:23:33)
at server.ts:41:21
No such file or directory (os error 2)
error: Uncaught Error: NotFound: No such file or directory (os error 2)
throw new Error(err);
^
at swaggerDoc (index.ts:38:11)
at server.ts:41:21
[denon] app crashed - waiting for file changes before starting ...

here's the repo

github.com/iampeters/deno-opine/bl...

Collapse
 
craigmorten profile image
Craig Morten • Edited

Hey Peters 😄 I fell into the same problems initially as well, the key is around apis array which you provide in the options object that is then passed to the swaggerDoc() method.

Unfortunately the swagger doc module doesn't perform auto discovery of files, you need to list them in the array. You also need to take care with the path used, as it expects the path to be relative to the current working directory where the deno run command is executed.

Looking at your example it looks like you still have references to Opine example files (REF: github.com/iampeters/deno-opine/bl...). Hopefully amending this array to mirror your repo should fix your problem. E.g. providing ./server.ts, and your paths under ./src/.

Thread Thread
 
iampeters profile image
Peters Chikezie

thanks for this. I have been able to get it to work. However, is there a swagger-UI that can be used in place of this JSON that is being served? with express, I can use swagger-UI-express and swagger-jsdoc to serve HTML to users. I am just curious.

Thread Thread
 
craigmorten profile image
Craig Morten

Unfortunately afaik this particular module is just for swagger JSON generation and doesn’t support the swagger UI as far as I know.

I don’t think there is a Deno module that can do this just yet - I might look to port the swagger UI node module so this can be done (unless someone has already done it!)

If you find something that can do it in Deno, please let me know!

Thread Thread
 
iampeters profile image
Peters Chikezie

oh ok. thanks. May I ask how you port these modules, I would love to learn so I can port some for myself 😜

Thread Thread
 
craigmorten profile image
Craig Morten

That would awesome for you to get involved! It depends very much on the module you wish to port.

  1. If it is already ESM and typescript, then you can just import it directly using the raw url!
  2. If it has no dependency on Node APIs in its code or subdeps then you can use JSPM CDN to import.
  3. Otherwise you need to port it - there are some tools to help, the rest is manually converting commonjs to ESM and swapping out Node methods for Deno ones.

Also check out my post on Reddit reddit.com/r/Deno/comments/h9ejsk/... for some more resources.

I might look to write a post on it if think would be useful?

Thread Thread
 
iampeters profile image
Peters Chikezie

thanks that would be awesome. I have been looking for a way to contribute to open-source but don't know where to start. You have been a great help so far.

Thread Thread
 
craigmorten profile image
Craig Morten

Generally it's a case of "I need to do a thing", seeing if it exists. If not then go code it and put it out there! If it helps you it'll help others 😄.

Check out things like firsttimersonly.com/ as well - there is a whole community worth of projects and issues geared towards helping people get involved in Open Source.

Collapse
 
iampeters profile image
Peters Chikezie

can it be integrated with swagger docs, I see swagger is available with Oak

Collapse
 
craigmorten profile image
Craig Morten

Ooh didn’t know that had supported that for Oak - do you have a link? I can’t find anything concrete but my Googling skills may be failing me 🙃 it certainly can be added if it doesn’t already work!

For instance, the deno_swagger_doc module should be immediately compatible. If you’re talking about generating server code from a swagger doc, that will take a little bit of work likely.

Collapse
 
iampeters profile image
Peters Chikezie • Edited

Deno_swagger_doc is the module I saw. The example is in Koa. I have tried to use opine to replicate it but I keep getting 404 error. This error is related to swagger_json. I will post the error here when I get to my laptop. I want users to be served the swagger docs when the hit / on my test deno server.

Thread Thread
 
craigmorten profile image
Craig Morten

I will look to add an example to the repo at some point later when have some free time!

Thread Thread
 
iampeters profile image
Peters Chikezie

Great... I'm looking forward to it.

Thread Thread
 
craigmorten profile image
Craig Morten

Sorry for the delay - check out the new swagger example here --> github.com/asos-craigmorten/opine/...

Thread Thread
 
iampeters profile image
Peters Chikezie

Great... Let me take a look