Skip to content

Instantly share code, notes, and snippets.

@joepie91
Last active February 20, 2024 20:52
Show Gist options
  • Save joepie91/c0069ab0e0da40cc7b54b8c2203befe1 to your computer and use it in GitHub Desktop.
Save joepie91/c0069ab0e0da40cc7b54b8c2203befe1 to your computer and use it in GitHub Desktop.
Rendering pages server-side with Express (and Pug)

Terminology

  • View: Also called a "template", a file that contains markup (like HTML) and optionally additional instructions on how to generate snippets of HTML, such as text interpolation, loops, conditionals, includes, and so on.
  • View engine: Also called a "template library" or "templater", ie. a library that implements view functionality, and potentially also a custom language for specifying it (like Pug does).
  • HTML templater: A template library that's designed specifically for generating HTML. It understands document structure and thus can provide useful advanced tools like mixins, as well as more secure output escaping (since it can determine the right escaping approach from the context in which a value is used), but it also means that the templater is not useful for anything other than HTML.
  • String-based templater: A template library that implements templating logic, but that has no understanding of the content it is generating - it simply concatenates together strings, potentially multiple copies of those strings with different values being used in them. These templaters offer a more limited feature set, but are more widely usable.
  • Text interpolation / String interpolation: The insertion of variable values into a string of some kind. Typical examples include ES6 template strings, or this example in Pug: Hello #{user.username}!
  • Locals: The variables that are passed into a template, to be used in rendering that template. These are generally specified every time you wish to render a template.

Pug is an example of a HTML templater. Nunjucks is an example of a string-based templater. React could technically be considered a HTML templater, although it's not really designed to be used primarily server-side.

View engine setup

Assuming you'll be using Pug, this is simply a matter of installing Pug...

npm install --save pug

... and then configuring Express to use it:

let app = express();

app.set("view engine", "pug");

/* ... rest of the application goes here ... */

You won't need to require() Pug anywhere, Express will do this internally.

You'll likely want to explicitly set the directory where your templates will be stored, as well:

let app = express();

app.set("view engine", "pug");
app.set("views", path.join(__dirname, "views"));

/* ... rest of the application goes here ... */

This will make Express look for your templates in the "views" directory, relative to the file in which you specified the above line.

Rendering a page

homepage.pug:

html
    body
        h1 Hello World!
        p Nothing to see here.

app.js:

router.get("/", (req, res) => {
    res.render("homepage");
});

Express will automatically add an extension to the file. That means that - with our Express configuration - the "homepage" template name in the above example will point at views/homepage.pug.

Rendering a page with locals

homepage.pug:

html
    body
        h1 Hello World!
        p Hi there, #{user.username}!

app.js:

router.get("/", (req, res) => {
    res.render("homepage", {
        user: req.user
    });
});

In this example, the #{user.username} bit is an example of string interpolation. The "locals" are just an object containing values that the template can use. Since every expression in Pug is written in JavaScript, you can pass any kind of valid JS value into the locals, including functions (that you can call from the template).

For example, we could do the following as well - although there's no good reason to do this, so this is for illustratory purposes only:

homepage.pug:

html
    body
        h1 Hello World!
        p Hi there, #{getUsername()}!

app.js:

router.get("/", (req, res) => {
    res.render("homepage", {
        getUsername: function() {
            return req.user;
        }
    });
});

Using conditionals

homepage.pug:

html
    body
        h1 Hello World!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

app.js:

router.get("/", (req, res) => {
    res.render("homepage", {
        user: req.user
    });
});

Again, the expression in the conditional is just a JS expression. All defined locals are accessible and usable as before.

Using loops

homepage.pug:

html
    body
        h1 Hello World!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

        p Have some vegetables:

        ul
            for vegetable in vegetables
                li= vegetable

app.js:

router.get("/", (req, res) => {
    res.render("homepage", {
        user: req.user,
        vegetables: [
            "carrot",
            "potato",
            "beet"
        ]
    });
});

Note that this...

li= vegetable

... is just shorthand for this:

li #{vegetable}

By default, the contents of a tag are assumed to be a string, optionally with interpolation in one or more places. By suffixing the tag name with =, you indicate that the contents of that tag should be a JavaScript expression instead.

That expression may just be a variable name as well, but it doesn't have to be - any JS expression is valid. For example, this is completely okay:

li= "foo" + "bar"

And this is completely valid as well, as long as the randomVegetable method is defined in the locals:

li= randomVegetable()

Request-wide locals

Sometimes, you want to make a variable available in every res.render for a request, no matter what route or middleware the page is being rendered from. A typical example is the user object for the current user. This can be accomplished by setting it as a property on the res.locals object.

homepage.pug:

html
    body
        h1 Hello World!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

        p Have some vegetables:

        ul
            for vegetable in vegetables
                li= vegetable

app.js:

app.use((req, res, next) => {
    res.locals.user = req.user;
    next();
});

/* ... more code goes here ... */

router.get("/", (req, res) => {
    res.render("homepage", {
        vegetables: [
            "carrot",
            "potato",
            "beet"
        ]
    });
});

Application-wide locals

Sometimes, a value even needs to be application-wide - a typical example would be the site name for a self-hosted application, or other application configuration that doesn't change for each request. This works similarly to res.locals, only now you set it on app.locals.

homepage.pug:

html
    body
        h1 Hello World, this is #{siteName}!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

        p Have some vegetables:

        ul
            for vegetable in vegetables
                li= vegetable

app.js:

app.locals.siteName = "Vegetable World";

/* ... more code goes here ... */

app.use((req, res, next) => {
    res.locals.user = req.user;
    next();
});

/* ... more code goes here ... */

router.get("/", (req, res) => {
    res.render("homepage", {
        vegetables: [
            "carrot",
            "potato",
            "beet"
        ]
    });
});

The order of specificity is as follows: app.locals are overwritten by res.locals of the same name, and res.locals are overwritten by res.render locals of the same name.

In other words: if we did something like this...

router.get("/", (req, res) => {
    res.render("homepage", {
        siteName: "Totally Not Vegetable World",
        vegetables: [
            "carrot",
            "potato",
            "beet"
        ]
    });
});

... then the homepage would show "Totally Not Vegetable World" as the website name, while every other page on the site still shows "Vegetable World".

Rendering a page after asynchronous operations

homepage.pug:

html
    body
        h1 Hello World, this is #{siteName}!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

        p Have some vegetables:

        ul
            for vegetable in vegetables
                li= vegetable

app.js:

app.locals.siteName = "Vegetable World";

/* ... more code goes here ... */

app.use((req, res, next) => {
    res.locals.user = req.user;
    next();
});

/* ... more code goes here ... */

router.get("/", (req, res) => {
    return Promise.try(() => {
        return db("vegetables").limit(3);
    }).map((row) => {
        return row.name;
    }).then((vegetables) => {
        res.render("homepage", {
            vegetables: vegetables
        });
    });
});

Basically the same as when you use res.send, only now you're using res.render.

Template inheritance in Pug

It would be very impractical if you had to define the entire site layout in every individual template - not only that, but the duplication would also result in bugs over time. To solve this problem, Pug (and most other templaters) support template inheritance. An example is below.

layout.pug:

html
    body
        h1 Hello World, this is #{siteName}!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

        block content
            p This page doesn't have any content yet.

homepage.pug:

extends layout

block content
    p Have some vegetables:

    ul
        for vegetable in vegetables
            li= vegetable

app.js:

app.locals.siteName = "Vegetable World";

/* ... more code goes here ... */

app.use((req, res, next) => {
    res.locals.user = req.user;
    next();
});

/* ... more code goes here ... */

router.get("/", (req, res) => {
    return Promise.try(() => {
        return db("vegetables").limit(3);
    }).map((row) => {
        return row.name;
    }).then((vegetables) => {
        res.render("homepage", {
            vegetables: vegetables
        });
    });
});

That's basically all there is to it. You define a block in the base template - optionally with default content, as we've done here - and then each template that "extends" (inherits from) that base template can override such blocks. Note that you never render layout.pug directly - you still render the page layouts themselves, and they just inherit from the base template.

Things of note:

  • Overriding a block is optional. If you don't override a block, it will simply contain either the default content from the base template (if any is specified), or no content at all (if not).
  • You can have an unlimited number of blocks with different names - for example, the one in our example is called content. You can decide to override any of them from a template, all of them, or none at all. It's up to you.
  • You can nest multiple blocks with different names. This can be useful for more complex layout variations.
  • You can have multiple levels of inheritance - any template you are inheriting from can itself inherit from another template. This can be especially useful in combination with nested blocks, for complex cases.

Static files

You'll probably also want to serve static files on your site, whether they are CSS files, images, downloads, or anything else. By default, Express ships with express.static, which does this for you.

All you need to do, is to tell Express where to look for static files. You'll usually want to put express.static at the very start of your middleware definitions, so that no time is wasted on eg. initializing sessions when a request for a static file comes in.

let app = express();

app.set("view engine", "pug");
app.set("views", path.join(__dirname, "views"));

app.use(express.static(path.join(__dirname, "public")));

/* ... rest of the application goes here ... */

Your directory structure might look like this:

your-project
|- node_modules ...
|- public
|  |- style.css
|  `- logo.png
|- views
|  |- homepage.pug
|  `- layout.pug
`- app.js

In the above example, express.static will look in the public directory for static files, relative to the app.js file. For example, if you tried to access https://your-project.com/style.css, it would send the user the contents of your-project/public/style.css.

You can optionally also specify a prefix for static files, just like for any other Express middleware:

let app = express();

app.set("view engine", "pug");
app.set("views", path.join(__dirname, "views"));

app.use("/static", express.static(path.join(__dirname, "public")));

/* ... rest of the application goes here ... */

Now, that same your-project/public/style.css can be accessed through https://your-project.com/static/style.css instead.

An example of using it in your layout.pug:

html
    head
        link(rel="stylesheet", href="/static/style.css")
    body
        h1 Hello World, this is #{siteName}!

        if user != null
            p Hi there, #{user.username}!
        else
            p Hi there, unknown person!

        block content
            p This page doesn't have any content yet.

The slash at the start of /static/style.css is important - it tells the browser to ask for it relative to the domain, as opposed to relative to the page URL.

An example of URL resolution without a leading slash:

  • Page URL: https://your-project.com/some/deeply/nested/page
  • Stylesheet URL: static/style.css
  • Resulting stylesheet request URL: https://your-project.com/some/deeply/nested/static/style.css

An example of URL resolution with the loading slash:

  • Page URL: https://your-project.com/some/deeply/nested/page
  • Stylesheet URL: /static/style.css
  • Resulting stylesheet request URL: https://your-project.com/static/style.css

That's it! You do the same thing to embed images, scripts, link to downloads, and so on.

@viitoormb
Copy link

Thank you!!

@joepie91
Copy link
Author

@ozgrozer Whoops, that was a bug in the guide. I've fixed it now. Thanks :)

@twhite96 You shouldn't request template files over HTTP directly; instead, you should make requests to routes that use the template files with a res.render call. The template rendering happens server-side, not client-side.

@PabloRegen
Copy link

Great explanation. Very clear. Thanks

@alexrqs
Copy link

alexrqs commented Sep 13, 2017

@joepie91 Thanks for the explanation, currently I'm looking for a way to serve html (for a few static pages on the site) with this

app.get('*', (req, res) => {
    res.render('static' + req.url)
})

// views/static/foo.pug

it works just fine until the template does not exist of course; so .. is there a way to detect if the template exist before I throw the render ? that way I'd be able to fallback to the 404 template.
pd: I know I can just read the filesystem but I dont want to.

@alexrqs
Copy link

alexrqs commented Sep 13, 2017

nvm, I just found it, in case anybody else is looking a way to do this, there is a callback for the render 🎉

app.get('*', (req, res) => {
  res.render('static' + req.url, function(err, html) {
    if(!err) { return res.send(html) }
    // Not super elegant the `indexOf` but useful
    if (err.message.indexOf('Failed to lookup view') !== -1) {
      return res.render('root/error')
    }
    throw err
  })
})

@Edrees
Copy link

Edrees commented Oct 20, 2017

👍 Thank you!

@penguin999
Copy link

This is a gold mine. Many thanks! 👍

@samuraijane
Copy link

+100, thank you!

@wlemahieu
Copy link

Great write-up. Much appreciated.

Copy link

ghost commented Feb 14, 2018

Hi,

I am passing a function from node to the view.

Called: functions.getArticles()

Bellow those not work: but this work: p Hi there, #{functions.getArticles()}!

I have tried different ways.

            span(class="btn--load-more--default btn--load-more--mobile" data-articleType="stories"
              onclick='' + functions.getArticles() + '')

@gwuah
Copy link

gwuah commented Apr 21, 2018

Thanks Man, this has everything I need

@affonso
Copy link

affonso commented Jun 20, 2018

@EddyVinck
Copy link

Learned a few things, thanks a lot!

@200001054
Copy link

BOT

@MiguelRipoll23
Copy link

Thank you!

@munair
Copy link

munair commented Jun 28, 2019

Excellent.

@kambl4
Copy link

kambl4 commented Jul 6, 2019

Thank you!!!!

@slidenerd
Copy link

Thank you! Quick question, how would you use Vue.js in this?

@joepie91
Copy link
Author

joepie91 commented Jul 8, 2019

@slidenerd Generally, you wouldn't. Vue is designed for building SPAs (Single Page Applications), where all navigation logic etc. is handled in custom client-side code, rather than by the server (this is generally not what you want). This article is instead about server-side templating, where the server is responsible for producing the HTML to show to the user, and the browser's native navigation features are used.

That having been said, there is eg. express-react-views to use JSX as a templating language for server-side templating in Express. It's possible that something similar exists for Vue as well, but as I don't use Vue, I don't have any specific recommendations. If it does exist, it would only support a small subset of what you might be used to from Vue; no event handling, for example (which isn't necessary when doing server-side templating).

@slidenerd
Copy link

@joepie91 thank you for replying back, it seems however that there is a technique to render .vue files from express server side. I was just curious to know your opinion on those

  1. Render Vue server side using express-vue
  2. Vue SSR

@joepie91
Copy link
Author

@slidenerd I have no opinion on those specific libraries, as I haven't used them. However, I would generally recommend looking for something that is really only designed as a templater (even if it uses Vue/React/whatever), as opposed to something that "pre-renders the first page load" and then does the rest through client-side JS.

Contrary to popular belief, SPAs are actually quite slow compared to strictly server-side rendering, and so you'll generally want to default to just rendering templates and not having any client-side JS at all.

Of course, if you have a specific reason to need client-side JS (like certain kinds of interactivity), then it's totally valid to use it, but it shouldn't be your default - and the same applies for "SSR" tools, which are often not really designed as server-side templaters, but rather as workarounds for the deficiencies of SPAs by rendering the absolute bare minimum on the server-side so that Google doesn't complain at you anymore. Such tools would not provide the user with a good experience, and should be avoided.

If those libraries are just designed as templaters (like express-react-views is), though, and you use them as templaters (rather than as something to 'pre-seed' your SPA), then they should be absolutely fine.

@NasreenUstad
Copy link

very helpful information... Thanks

@slidenerd
Copy link

slidenerd commented Nov 10, 2019

You can get the REPO here

Fastest SSR Framework Benchmarks!

Hi! The goal of this repository is to find out which is the fastest SSR framework with Vue.js. 3 Frameworks have been tested in this repo. If you want to add another framework to this list, feel free to submit a PR or open an ISSUE. If you have any doubts, check out the .MD files in the root of this directory on GitHub.

  1. Plain Express Server with Webpack to render Vue components
  2. Vue SSR Native
  3. NuxtJS SSR

Screenshots

Plain Express Server with Vue SSR: Concurrency 10 Requests 10000

Plain Express Server with Vue SSR

Native Vue SSR: Concurrency 10 Requests 10000

Native Vue SSR

Nuxt SSR: Concurrency 10 Requests 10000

Native Vue SSR

Setup

  • Best to run all tests on Virtual Box.
  • Download and install Virtual Box
  • Install Ubuntu Server 18.04 on Virtual Box.
  • Select the option to enable SSH while installing Ubuntu Server.
  • Update Ubuntu inside the server post installation by following instructions HERE
  • Reboot virtual box Ubuntu Server.
  • Install nvm following instructions HERE
  • Run the command below to find out the latest version of stable node.js available in nvm
nvm ls-remote
nvm install v10.16.3 (if it is the latest version)
  • Alternatively, you can directly install the latest stable version of node.js using the command below
nvm install --lts
  • Now use the installed version as shown below
nvm use v10.16.3
or 
nvm use --lts
  • Update npm for the current node.js version that you installed using the command below.
nvm install-latest-npm
  • Open Virtual Box Settings and go to Network
  • Set networking mode to Bridged Adapter, attached to en-1 Wifi Airport, allow all under advanced settings.
  • Reboot virtual box Ubuntu Server.
  • Check the status of the Uncomplicated FireWall with the command below.
sudo ufw status
  • If this is the first time you are setting things up , status will be inactive.
  • Allow ports 22, 80, 443, 9001, 9002, 9003 with the command below.
sudo ufw allow 22 80 443 9001 9002 9003
  • Enable the firewall with the command below.
sudo ufw enable
  • Inside the virtual machine, plain express server runs on port 8001, nuxt runs on port 3000 and vue ssr runs on port 8000.
  • Modify the etc/hosts file to add a local domain name which will be used for NGINX configuration with the command below.
sudo vi /etc/hosts
  • Add an entry there as shown below.
ssr.local 127.0.0.1
  • Externally when accesed from outside the virtual box server, plain express will run on 9001, vue native ssr will run on 9002 and nuxt will run on 9003.
  • Use the command below in the terminal of your Virtual Box Ubuntu Server to find the IP address of your Virtual Box Ubuntu Server which you can access from your HOST machine.
hostname -I
  • For me it says 192.168.1.104.
  • If using HTTPS on GitHub, clone the repo with the command below inside your home directory (cd $HOME if you dont know where that is...)
 git clone https://github.com/slidenerd/ssr_speed_test.git
 cd ssr_speed_test/
  • We run each server one at a time to get accurate answers
  • To run the express plain ssr server run the command below
cd test_plain_express_ssr/
npm i
npm run build && npm run start
  • To test if this is working run the command below inside Virtual Box or from your Terminal connected to Virtual Box via SSH
curl http://127.0.0.1:8001/
  • To run the vue ssr server go back to the root directory ssr_speed_test and run the command below
cd test_native_vue_ssr/
npm i
npm run build && npm run start
  • To test if this is working run the command below inside Virtual Box or from your Terminal connected to Virtual Box via SSH
curl http://127.0.0.1:8000/
  • To run the nuxt ssr server, go back to the root directory ssr_speed_test and run the command below
cd test_nuxt_ssr/
npm i
npm run build && npm run start
  • To test if this is working run the command below inside Virtual Box or from your Terminal connected to Virtual Box via SSH
curl http://127.0.0.1:3000/
  • In order to check if any of the packages are outdated, install node check updates from HERE
  • Go into any of our 3 servers root directory where package.json is present
  • Run the command below to find all outdated packages
ncu
  • If you are going to update, make a note of these versions just in case things dont work after updating
ncu -u
  • Run the command above to update all dependencies to their latest version and test each curl request to check if we are still golden
  • If you find any error, either try and fix it with stackoverflow or roll back to previous version which we noted earlier
  • To make any of our servers accessible from outside Virtual Box through our host for example, we are going to setup NGINX as a reverse proxy
  • Inside Virtual Box, run the command below to insall NGINX
sudo apt install nginx
  • Check if NGINX is on by the running the command below after installation
sudo systemctl status nginx

}

server {
listen 9002;
listen [::]:9002;

    server_name ssr.local
    root /home/hexa/ssr_speed_test/test_native_vue_ssr;

    #security
    include nginxconfig.io/security.conf;

    #reverse proxy
    location / {
            proxy_pass http://127.0.0.1:8000;
            include nginxconfig.io/proxy.conf;
    }

    # additional config
    include nginxconfig.io/general.conf;

}

server {
listen 9003;
listen [::]:9003;

    server_name ssr.local
    root /home/hexa/ssr_speed_test/test_nuxt_ssr;

    #security
    include nginxconfig.io/security.conf;

    #reverse proxy
    location / {
            proxy_pass http://127.0.0.1:3000;
            include nginxconfig.io/proxy.conf;
    }

    # additional config
    include nginxconfig.io/general.conf;

}
'''

Benchmarking Process

  • We are using the apache bench tool to do all the benchmarks. Have a better tool in mind? Feel free to submit a PR or open an issue.
  • It is preinstalled on OSX (host machine in my case), Type ab and see if you get an error
  • If it is not installed, you may need to find out how to install it
  • Below is a sample of how a test is done, we set concurrency to 1, number of requests to fire as 1000 and the option to not exit the test if we get any socket errors. Replace the IP address with yours
# for testing plain express ssr setup
ab -c 1 -n 1000 -g output_directory_with_tsv_files -r http://192.168.1.104:9001/ 

# for testing native vue ssr setup
ab -c 1 -n 1000 -g output_directory_with_tsv_files -r http://192.168.1.104:9002/

# for testing nuxt ssr setup
ab -c 1 -n 1000 -g output_directory_with_tsv_files -r http://192.168.1.104:9003/

  • Run the script ./run_benchmark.sh file to generate outputs for each server
  • The script takes 2 arguments, the full url where one of your servers is running (test one server at a time)
  • And the folder name inside which that server's details will be stored
  • Example is shown below
./run_benchmark.sh http://192.168.1.104:9001/ plain_express

Results

  • Check each of the apache benchmarks folder
  • It has a raw txt output from each benchmark, a tsv file, a graph and a scatter plot as discussed here http://www.bradlanders.com/2013/04/15/apache-bench-and-gnuplot-youre-probably-doing-it-wrong/
  • The tool used is Apache Bencharks 2.3 <$Revision: 1826891 $>
  • Virtual Box Version 6.0.10 r132072 (Qt5.6.3)
  • Host machine OSX High Sierra 10.13.6 (17G65)
  • Virtual Machine version: Ubuntu 18.04 Bionic 10 GB HDD, 1GB RAM (default settings for everything except Network where we used Bridged Adapter)
  • Did not enable Keep Alive for any of them (equal grounds for everyone)

@AsaoluElijah
Copy link

Awesome, thank you 😎

@OddAlgorithm
Copy link

This is still so useful! Helped me understand how to pass variables through express when extending layouts.

@Screwlim
Copy link

Thank you. This is a huge help for me 👍

@yozaam
Copy link

yozaam commented Oct 26, 2020

Thank you!

@ssyrota
Copy link

ssyrota commented Mar 8, 2021

Thanks. You cool!

@Mike74Mike
Copy link

Thank You

@yoyozi
Copy link

yoyozi commented Dec 16, 2022

Very very helpfull THANKS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment