DEV Community

Cover image for Opine Tutorial Part 2: Creating A Website In Deno
Craig Morten
Craig Morten

Posted on • Edited on

Opine Tutorial Part 2: Creating A Website In Deno

In this second Opine article we will be looking at how you can create a template website project which you can then take away to populate with your own specific routes and views.

Prerequisites: Opine Tutorial Part 1: Express For Deno

Objective: Start your own new website projects using Opine.


Overview

This article will cover how you can create a basic template for a website using Opine for Deno, which you will then be able to populate with your own views / templates and routes.

In the following sections we will walk through the development process step by step so you are able to recreate the same working application by the end of the tutorial. We will touch on some explanation about views and CSS, and how the application is structured. At the end, we will also cover how you can run your website template to verify that it is all working.

Creating the website template

Let's first take a look at the desired project structure and the directories and files we are going to need to create.

Directory structure

Create a new base project directory for your template website, and then create the following set of directories and files:

.
├── app.ts
├── deps.ts
├── entrypoint.ts
├── public/
│   ├── images/
│   ├── scripts/
│   └── stylesheets/
│       └── style.css
├── routes/
│   ├── index.ts
│   └── users.ts
└── views/
    ├── error.ejs
    └── index.ejs
Enter fullscreen mode Exit fullscreen mode

The deps.ts file defines the application dependencies that we will need. The entrypoint.ts sets up some of the application error handling and then loads app.js which contains our main Opine application code. The app routes that we will be using are stored in separate modules under the routes/ directory, and the templates are stored under the views/ directory.

The following sections describe each file in full detail.

deps.ts

This file collates all the dependencies we need for our Opine application. Think of it a little bit like a package.json from Node, but really it's just a helper module for re-exporting third parties - essentially a barrel file.

export { dirname, join } from "https://deno.land/std@0.65.0/path/mod.ts";
export { createError } from "https://deno.land/x/http_errors@2.1.0/mod.ts";
export {
  opine,
  json,
  urlencoded,
  serveStatic,
  Router,
} from "https://deno.land/x/opine@0.21.2/mod.ts";
export { Request, Response, NextFunction } from "https://deno.land/x/opine@0.21.2/src/types.ts";
export { renderFileToString } from "https://deno.land/x/dejs@0.8.0/mod.ts";
Enter fullscreen mode Exit fullscreen mode

There's not a lot to say here, but we can see we import some methods from the Deno standard library for path manipulation, an error message creation utility, our Opine methods and the dejs renderFileToString method so we can render ejs templates.

entrypoint.ts

This file is the main entrypoint to our application and will be module we target when running the server later on.

import app from "./app.ts";

// Get the PORT from the environment variables and store in Opine.
const port = parseInt(Deno.env.get("PORT") ?? "3000");
app.set("port", port);

// Get the DENO_ENV from the environment variables and store in Opine.
const env = Deno.env.get("DENO_ENV") ?? "development";
app.set("env", env);

// Start our Opine server on the provided or default port.
app.listen(port, () => console.log(`listening on port ${port}`));
Enter fullscreen mode Exit fullscreen mode

It provides some utility to get useful environment variables for PORT and DENO_ENV and ultimately calls the listen() method on our Opine application.

app.ts

This file holds the core of the application code for our template website, and performs the following functions:

  1. Configures Opine for handling ejs templates.
  2. Serves our static assets such as CSS for our pages.
  3. Mounts our routers for our root and users routes.
  4. Sets up a handler for not found routes, and an error handler that will send the error to our users.
import {
  dirname,
  join,
  createError,
  opine,
  json,
  urlencoded,
  serveStatic,
  Response,
  Request,
  NextFunction,
  renderFileToString,
} from "./deps.ts";
import indexRouter from "./routes/index.ts";
import usersRouter from "./routes/users.ts";

const __dirname = dirname(import.meta.url);

const app = opine();

// View engine setup
app.set("views", join(__dirname, "views"));
app.set("view engine", "ejs");
app.engine("ejs", renderFileToString);

// Handle different incoming body types
app.use(json());
app.use(urlencoded());

// Serve our static assets
app.use(serveStatic(join(__dirname, "public")));

// Mount our routers
app.use("/", indexRouter);
app.use("/users", usersRouter);

// catch 404 and forward to error handler
app.use((req, res, next) => {
  next(createError(404));
});

// Error handler
app.use(function (err: any, req: Request, res: Response, next: NextFunction) {
  // Set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

  // Render the error page
  res.setStatus(err.status || 500);
  res.render("error");
});

export default app;
Enter fullscreen mode Exit fullscreen mode

Routes

Within our routes/ folder we have two files, one for each route that we support. Namely:

  1. An index.ts for handling requests to our root path /.
  2. A users.ts for handling requests to our users path /users.

The routers that are created in these files are imported and attached to our Opine server in the app.ts as we have seen previously.

Let's look at each file individually!

index.ts

Our index.ts sets up an Opine Router for our root path /. Here we make use the of the built-in render() functionality to return a rendered ejs template to our users.

Specifically we tell the router to render the index.ejs file with a title value set to Opine. Check out the index.ejs in the views/ directory to see how this title value will be used.

import { Router } from "../deps.ts";

const router = Router();

// GET home page.
router.get("/", (req, res, next) => {
  res.render("index", {
    title: "Opine"
  });
});

export default router;
Enter fullscreen mode Exit fullscreen mode

users.ts

Here we set up another Router, similar to how we did for our root view.

In this case, we haven't set up a view / template to render, but instead are just returning a placeholder to the user.

One thing to note is how the router handler's path is set to / and not /users as we might expect. This because we actually configure the /users part of the path in our app.ts when we mount our Router. The path segments configured in router handlers are appended to the base paths set in app.use() when they are mounted.

import { Router } from "../deps.ts";

const router = Router();

// GET users listing.
router.get("/", (req, res, next) => {
  res.send("Users are coming shortly!");
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Public

We also have a public/ directory which contains some further directories for styles, scripts and images.

The scripts/ and images/ directories are just there as placeholders for now, but we have defined a styles.css in the stylesheets/ directory, and we have seen this used in both our of templates.

styles.css

Here we define some basic styles to make our template website look a little neater than just the browser defaults!

body {
  padding: 10px 25px;
  font-size: 14px;
  font-family: "Helvetica Nueue", "Lucida Grande", Arial, sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

Specifically we add some padding and set some nice fonts for our template website's text.

Views (templates)

The views / templates are stored in the views/ directory, which was specified in our app.js. Here we are using ejs templates as we have opted to use dejs as our rendering engine (also configured in the app.js).

Let's step though our templates.

index.ejs

This template is used by our router defined in ./routes/index.js to render a homepage for our application on the root path /.

We can see that it is expecting a title variable, which is passed in the options in our res.render() method we've looked at previously.

It also pulls in the style.css from our ./public/stylesheets/ directory that we have set to serve as a static asset in our app.ts.

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %>!</p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

error.ejs

This ejs defines our error template that we render in our error handler in our app.js whenever there is an error in our application.

<!DOCTYPE html>
<html>
  <head>
    <title><%= message %></title>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>
  <body>
    <h1><%= message %></h1>
    <h2><%= error.status %></h2>
    <pre><%= error.stack %></pre>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

It is largely similar to the index.ejs template we looked at above, but instead of a title, it expects a message string and an error object. If you re-visit the error handler in app.ts to check out how we achieved this you might be confused a first - we don't pass the message orerrorto theres.render()` method!

What you will notice is that we set both of those values onto the res.locals object. The locals property is a special object which is preserved and passed between middlewares, and it is also made available to the res.render() method, meaning all of it's properties are available to be used in the template.

Running our website template

We now have created and reviewed all of the files we need to be able to run a basic website template with some custom views and routes.

We can now run our application using the deno run command as follows:

deno run --allow-net --allow-env --allow-read=./ ./entrypoint.ts

Here we have added the --allow-net flag to allow our server to access the network, the --allow-env flag so our app can read environment variables, and a --allow-read flag scoped to our current working directory ./ so our application is able to read our template views and our static files in our public/ directory.

If you open http://localhost:3000/ you should be our rendered homepage template!

Template website homepage with the text

Navigating to http://localhost:3000/users we can see our placeholder response.

Template website users page with placeholder response

And if we navigate to an invalid route, such as http://localhost:3000/invalid, we can see that our error handling middleware is working correctly, and is returning the 404 Not Found error message back to us.

Template website displaying 404 Not Found on an invalid route

Challenge yourself

Why not take the template website we have made and try and do the following:

  1. Set a custom port using a PORT environment variable and check that our server starts on the custom port.
  2. Set a DENO_ENV to something other than development and see what happens to our error pages - can you find where in the code is responsible for this difference?
  3. Why not try setting up the server to restart when a file changes by using the Denon module.
  4. Create a new route in ./routes/users.ts that will display the text "Hello Deno!" at URL /users/deno/. Test it by running the server and visiting http://localhost:3000/users/deno/ in your browser.

And if you're up for a real challenge, why not try and link the ./routes/users.ts router code up to a database of your choice (it can just be a mock JSON database!)? If you need some pointers, you can check out this article which talks through how to set up a mock database for a simple Opine API.

Summary

You have now created a template website project and verified that it runs using Deno. Great job! 🎉

Importantly, you hopefully also now understand how the project is structured, so you have a good idea where you need to make changes if you want to add your own routes and views for your own bespoke website.

That's all for this one gang! Thanks for reading and good luck with your Deno journey!

Have questions, comments or suggestions? I love to hear them so drop them in the section below!


Inspiration: This article draws some pointers from the Express web framework series, but ultimately diverges as the Opine CLI (application generator) is covered in the next article!

Top comments (8)

Collapse
 
futurelucas4502 profile image
Lucas Wilson

Hi there so I'm brand new to deno and attempted to make an app using opine and dejs without specifying dependency versions however got the following error:

Cannot read property 'req' of undefined
TypeError: Cannot read property 'req' of undefined
at redirect (serveStatic.ts:220:39)
at serveStatic (serveStatic.ts:124:14)
at async Layer.handle as handle_request

and after following your tutorial I get a different error that can be seen in the image below:

preferably id like to use the latest versions of all the packages but it seems even using the old ones don't work do you have any suggestions of things for me to try?

Collapse
 
craigmorten profile image
Craig Morten

Hey Lucas 👋

Sorry to hear you've been having issues! Unfortunately Deno is evolving very quickly at the moment and some of these posts become outdated very quickly as breaking changes are introduced!

I will update this article with working examples today and message again once done! It looks like I wrote this prior to some big changes to the deno.land/x registry which means you can no longer reference commits as the version for packages (see the url for the vary package you have in your image).

Thanks for raising this 😄

Collapse
 
futurelucas4502 profile image
Lucas Wilson

Hi there,

Thanks for your quick response as per what you said I've tried removing the version numbers entirely so now my deps file looks like this:

export { dirname, join } from "https://deno.land/std/path/mod.ts";
export { createError } from "https://deno.land/x/http_errors/mod.ts";
export {
  opine,
  json,
  urlencoded,
  serveStatic,
  Router,
} from "https://deno.land/x/opine/mod.ts";
export { Request, Response, NextFunction } from "https://deno.land/x/opine/src/types.ts";
export { renderFileToString } from "https://deno.land/x/dejs/mod.ts";

But like before when I try visiting the site I get the following error

Cannot read property 'req' of undefined
TypeError: Cannot read property 'req' of undefined
at redirect (serveStatic.ts:220:39)
at serveStatic (serveStatic.ts:124:14)
at async Layer.handle as handle_request

As you can see the error really isn't very helpful so I'm not 100% sure whats going on other than it would seem something is breaking with Opine as the serveStatic function is the one erroring hopefully since your the developer of Opine you'll have some insight to whats breaking

Many thanks
Lucas WIlson

Thread Thread
 
craigmorten profile image
Craig Morten

Hey Lucas, removing the versions is strongly discouraged by the Deno maintainers, and may not work at all in future if not already. As far as I can tell the fix is to update the module versions to their latest (check out the updated deps.ts file in the post)

Collapse
 
craigmorten profile image
Craig Morten

I've updated the article to include working versions in the deps.ts for Deno version 1.3.0 and can confirm it works locally for me - please reach out if have any further issues 😄

Thread Thread
 
futurelucas4502 profile image
Lucas Wilson

Hi there,

I'm afraid even after updating the deps.ts file I still have the same problem i have however managed to narrow down the cause of the issue it seems to occur when the following line of code is run:

// Serve our static assets
app.use(serveStatic(join(__dirname, "public")));

After looking at your github repo for Opine it would seem that this line is the one causing problems with "this" returning as undefined therefore causing an error as I am really not an expert I'm not sure whats going on however if need be I can open an issue on the repo with more information if required

Thread Thread
 
craigmorten profile image
Craig Morten

Heya, unfortunately I can't reproduce your issue (always the worst when trying to help! 😂) - please raise an issue on the GitHub repo and we can work it out from there? 🚀

Thread Thread
 
futurelucas4502 profile image
Lucas Wilson

Hi there,

The issue has been created and thank you for all your help so far it is always tricky when your not able to reproduce the issue 😂