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");
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
andres
are used. You can also use object destructing to just get the property or method of thereq
orres
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@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/
.
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@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:
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:
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.
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)
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
.Yes you are correct - thanks for pointing it out, Will update 😄
Update: all changed - thanks!
this is awesome. looks very easy to use. thanks
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...
Hey Peters 😄 I fell into the same problems initially as well, the key is around
apis
array which you provide in theoptions
object that is then passed to theswaggerDoc()
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/
.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.
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!
oh ok. thanks. May I ask how you port these modules, I would love to learn so I can port some for myself 😜
That would awesome for you to get involved! It depends very much on the module you wish to port.
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?
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.
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.
can it be integrated with swagger docs, I see swagger is available with Oak
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.
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.
I will look to add an example to the repo at some point later when have some free time!
Great... I'm looking forward to it.
Sorry for the delay - check out the new swagger example here --> github.com/asos-craigmorten/opine/...
Great... Let me take a look