Scaling an Express.js Application with Memcache on DigitalOcean
In this guide, we’ll explore how to create a simple Express 4 application, deploy it using DigitalOcean, then add Memcache to alleviate a performance bottleneck.
Memcache is a technology that improves the performance and scalability of web apps and mobile app backends. You should consider using Memcache when your pages are loading too slowly or your app is having scalability issues. Even for small sites, Memcache can make page loads snappy and help future-proof your app.
Prerequisites
Before you complete the steps in this guide, make sure you have all of the following:
- Familiarity with Node.js (and ideally Express.js)
- A DigitalOcean account.
- If you like managing DigitalOcean resource via the CLI, you need the
doctl
installed and configured. - Node.js,
npm
, andgit
installed on your computer.
Deploying an Express.js application to DigitalOcean
Express.js is a minimalist framework that doesn’t require an application skeleton. To make things easier for you, we’ve got a basic example application set up here.
We’ll be using Github to track file changes in DigitalOcean, so if you’d like to
follow along, go ahead and fork that repository to your personal account, and
clone your fork to your local machine. Then cd
into the
examples-expressjs
directory.
$ git clone git@github.com:memcachier/examples-expressjs.git
$ cd examples-expressjs
$ git checkout digital-ocean
The master
branch in the repository is specifically for deploying to Heroku.
You can find that
tutorial here.
Next, install all of the required packages using:
$ npm install
To simplify development, we’ve use a template engine, ejs
, but you can use
whichever engine you prefer, including mustache
, pug
, or nunjucks
.
Now that we’ve installed all the packages we need, we can add our app code. We’ll create a page that calculates the largest prime number that’s smaller than a number a visitor submits.
Open up app.js
and add the following code into the section
labeled ADD THE DIY CODE HERE
:
/* ADD THE DIY CODE HERE */
// Super simple algorithm to find largest prime <= n
var calculatePrime = function(n){
var prime = 1;
for (var i = n; i > 1; i--) {
var is_prime = true;
for (var j = 2; j < i; j++) {
if (i % j == 0) {
= false;
is_prime break;
}
}if (is_prime) {
= i;
prime break;
}
}return prime;
}
// Set up the GET route
.get('/', function (req, res) {
appif(req.query.n) {
// Calculate prime and render view
var prime = calculatePrime(req.query.n);
.render('index', { n: req.query.n, prime: prime});
res
}else {
// Render view without prime
.render('index', {});
res
};
})
/* END DIY CODE */
Now let’s add a corresponding view. Open up the file views/index.ejs
and copy
the following ejs
-enhanced HTML into it:
<!-- ADD DIY INDEX.HTML CODE HERE -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<title>Express.js caching example</title>
</head>
<body>
<div class="container">
<h1>
Express.js caching example</h1>
<p class="lead">For any number N (max 10000), we'll find the largest prime number
less than or equal to N.</p>
<!-- Form to submit a number -->
<form class="form-inline" action="/">
<input type="text" class="form-control" name="n" />
<input type="submit" class="btn btn-primary" value="Find Prime" />
</form>
<hr>
<!-- Show the result -->
<% if (locals.prime) { %>
<div class="alert alert-primary">
<p class="lead">Largest prime less or equal than <%= n %> is <%= prime %></p>
</div>
<% } %>
<!-- TODO: Error handling -->
</div>
</body>
</html>
You now have a working app that you can start locally by running npm start
.
Create an Express One-Click application
Update: One-click-apps are now Marketplace images.
To build an app we first need a droplet. Either go to your DigitalOcean dashboard and create one or launch one via the CLI:
$ doctl compute droplet create express-memcache --image nodejs-18-04 --region nyc1 --size s-1vcpu-1gb --ssh-keys <KEY_FINGERPRINT>
Give the droplet a minute to come up and then look up its IP via the dashboard or by typing
$ doctl compute droplet list
Now you can login to your droplet via
$ ssh root@<DROPLET_IP>
Visiting the page at http://<DROPLET_IP>/
isn’t currently working. Let’s fix
that.
Create a new user
For security reasons, we need to create another user besides root
. Start by
logging (ssh’ing) into your droplet and typing the following commands:
root@express-memcache:~\# adduser linzjax # or whatever username you'd like.
# We'll need to give our new user sudo permissions:
root@express-memcache:~\# usermod -aG sudo linzjax
# Then finally switch over to that user
root@express-memcache:~\# su linzjax
Next we’ll need to give our new user ssh permissions:
linzjax@express-memcache:~$ mkdir .ssh
linzjax@express-memcache:~$ sudo cp /root/.ssh/authorized_keys .ssh/
linzjax@express-memcache:~$ sudo chown linzjax:linzjax .ssh/authorized_key
We can now login to our droplet with our new user from our terminal:
$ ssh linzjax@<DROPLET_IP>
Set up your application on DigitalOcean
In order to get a copy of your application into the droplet, either clone your GitHub repo you forked at the beginning of the tutorial.
linzjax@express-memcache:~$ git clone https://github.com/<username>/examples-expressjs.git
Install pm2 to handle app initiation on startup.
linzjax@express-memcache:~/$ cd examples-express linzjax@express-memcache:~/examples-express$ sudo npm install -g pm2 linzjax@express-memcache:~/examples-express$ pm2 start app.js # You'll get a message indicating that the application has started. # To test this you can run `curl http://localhost:3000` in the root user window.
pm2 can be set up to launch when ever the application starts or reboots.
linzjax@express-memcache:~/examples-express$ pm2 startup systemd # follow the directions to copy and paste the `sudo env PATH=$PATH` line into # your terminal.
Install Nginx and configure to listen to port 3000.
linzjax@express-memcache:~$ sudo apt-get update linzjax@express-memcache:~$ sudo apt-get install nginx
Once Nginx is install, we’ll need to open the firewall to allow http requests.
linzjax@express-memcache:~$ sudo ufw allow 'Nginx HTTP'
We’ll configure Nginx to serve requests to port 3000.
linzjax@express-memcache:~$ sudo vim /etc/nginx/sites-available/default
Copy and paste the following settings into the location section.
location \{ proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://localhost:8888/; proxy_redirect off; }
Type
:wq
to save and exit vim. Now all that’s left is to check that our Nginx configuration is correct, and restart it.linzjax@express-memcache:~$ sudo nginx -t # Should get the following message. nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful linzjax@express-memcache:~$ sudo systemctl restart nginx
Now we can visit our DigitalOcean IP address in our browser and you should be able to see our app!
If you’re getting error messages, you can explore the logs by using
pm2 log
.
Learn to write Express.js middleware
Our prime-calculating app works, but it has one mayor flaw: a user can submit invalid input, such as a string of letters. To validate the input, we’ll create middleware in Express.
There are several validation middleware packages available for Express, and you should use one of those in most cases. In this tutorial, we create our own validation for demonstration purposes.
Express middleware typically consists of a chain of functions that inspect and potentially modify the details of a request and its corresponding response. Each function takes three parameters:
- The
request
object - The
response
object - A
next
function that represents the next middleware function in the chain
Each middleware function can modify the request
and response
objects as
necessary. After doing so, it can either call the next
middleware function or
return
to terminate the chain prematurely.
For our app, we create a validation middleware function that parses the submitted query and checks whether it’s a number below 10000.
- If it is, the function calls
next
. - If it isn’t, the function
return
s an error response.
Add this function to app.js
and call it when processing the GET
route:
// ...
var validate = function(req, res, next) {
if(req.query.n) {
= parseInt(req.query.n, 10);
number if(isNaN(number) || number < 1 || number > 10000){
.render('index', {error: 'Please submit a valid number between 1 and 10000.'});
resreturn;
}.query.n = number;
req
}next();
}
.get('/', validate, function (req, res) {
app// ...
})// ...
The validation middleware might return an error message, which we need to display in the index.ejs
view:
<!-- Show the result -->
<!-- ... -->
<!-- Error handling -->
<% if (locals.error) { %>
<div class="alert alert-danger">
<p class="lead"><%= error %></p>
</div>
<% } %>
Commit and deploy your changes:
$ git commit -am 'Add input validation'
# This is why you should make a fork of the repo, rather than just cloning ours ;)
$ git push -u origin master
$ ssh linzjax@<DROPLET_IP>
linzjax@express-memcache:~$ cd examples-express
linzjax@express-memcache:~/examples-express$ git pull origin master
linzjax@express-memcache:~/examples-express$ pm2 restart app.js
Open the app and submit some invalid queries to see the error message in action.
Adding caching to Express
Memcache is an in-memory, distributed cache. Its primary API consists of two
operations: SET(key, value)
and GET(key)
. Memcache is like a hashmap
(or dictionary) that is spread across multiple servers, where operations are
still performed in constant time.
The most common use for Memcache is to cache expensive database queries and HTML renders so that these expensive operations don’t need to happen over and over again.
Set up Memcache
To use Memcache in Express, you first need to provision an actual Memcache
cache. MemCachier provides a fast and flexible
multi-tenant cache system that’s compatible with the protocol used by the
popular memcached
software. When you create a
cache with MemCachier, you’re provided with one or more endpoints that you can
connect to using the memcached
protocol, accessing your cache just as if you
had set up your own memcached
server. So head over to
https://www.memcachier.com, sign up for an account, and create a free
development cache. If you need help getting it set up,
follow the directions here.
There are three config vars to you’ll need for your application to be able to
connect to your cache: MEMCACHIER_SERVERS
, MEMCACHIER_USERNAME
, and
MEMCACHIER_PASSWORD
. Let’s add them to a .env
file in your local files.
$ vim .env
Add the following variables to your .env file:
MEMCACHIER_USERNAME=<username>
MEMCACHIER_PASSWORD=<password>
MEMCACHIER_SERVERS=<servers>
We’ll need to install dotenv
so our application will read our .env
file.
$ npm install dotenv
and configure it at the very top of app.js
:
require('dotenv').config();
Once your environment variables are set up, next step is to install memjs
with npm
so we can use caching in Express.
$ npm install memjs
and configure it in app.js
:
// ...
var memjs = require('memjs')
var mc = memjs.Client.create(process.env.MEMCACHIER_SERVERS, {
failover: true, // default: false
timeout: 1, // default: 0.5 (seconds)
keepAlive: true // default: false
})
// ...
Caching expensive computations
There are two reasons why caching the results of expensive computations is a good idea:
- Pulling the results from the cache is much faster, resulting in a better user experience.
- Expensive computations use significant CPU resources, which can slow down the rest of your app.
Our prime number calculator doesn’t really have any expensive computations, because we limit the input value to 10000. For the sake of the tutorial, however, let’s assume that calculating the prime is an expensive computation we would like to cache.
To achieve this, let’s modify the GET
route in app.js
as follows:
// ...
.get('/', validate, function (req, res) {
appif(req.query.n) {
var prime;
var prime_key = 'prime.' + req.query.n;
// Look in cache
.get(prime_key, function(err, val) {
mcif(err == null && val != null) {
// Found it!
= parseInt(val)
prime
}else {
// Prime not in cache (calculate and store)
= calculatePrime(req.query.n)
prime .set(prime_key, '' + prime, {expires:0}, function(err, val){/* handle error */})
mc
}// Render view with prime
.render('index', { n: req.query.n, prime: prime });
res
})
}else {
// Render view without prime
.render('index', {});
res
};
})
// ...
Push these changes to Github and pull them into your droplet. Then submit some numbers to find primes:
$ git commit -am 'Add caching'
$ git push
$ ssh linzjax@<DROPLET_IP>
linzjax@express-memcache:~$ cd examples-express
linzjax@express-memcache:~/examples-express$ git pull origin master
linzjax@express-memcache:~/examples-express$ pm2 restart app.js
linzjax@express-memcache:~/examples-express$ pm2 log
The page should work just as before. However, under the hood, already calculated primes are now cached. To see what’s going on in your cache, open the MemCachier dashboard (which is where you found your environment variables.)
On the dashboard you can refresh the stats each time you request a prime.
The first time you enter a number, the get misses
will increase. For any
subsequent request of the same number, you should get an additional get hit
.
If it’s not working, and you’re getting the following error:
Error: connection ECONNREFUSED 12.0.0.1:11211.
Check and make sure that your MEMCACHIER_* ENV
variables are set correctly. If
it’s still not working, please note: Only MemCachier caches with DigitalOcean
as the provider will work with DigitalOcean droplets.
Caching rendered views
Rendering HTML views is generally an expensive computation, and you should
cache rendered views whenever possible. In Express, you can achieve this easily
with middleware. Let’s add a cacheView
middleware function to app.js
that
checks whether the view for a given URL (including query parameters) is in the
cache.
- If it is, the view is sent immediately from the cache.
- If not, we wrap the
send
function in the response object to cache the rendered view and call thenext
function.
// ...
var cacheView = function(req, res, next) {
var view_key = '_view_cache_' + req.originalUrl || req.url;
.get(view_key, function(err, val) {
mcif(err == null && val != null) {
// Found the rendered view -> send it immediately
.send(val.toString('utf8'));
resreturn;
}// Cache the rendered view for future requests
.sendRes = res.send
res.send = function(body){
res.set(view_key, body, {expires:0}, function(err, val){/* handle error */})
mc.sendRes(body);
res
}next();
;
})
}
.get('/', validate, cacheView, function (req, res) {
app// ...
;
})//..
This is easy enough and works well. However, if the view ever changes, we need
to be careful. To illustrate the case of a changing page, let’s add a “Like”
button to each number and its calculated largest prime. Let’s put the button
just below the calculated prime in the index.ejs
file:
<!-- ... -->
<!-- Show the result -->
<% if (locals.prime) { %>
<div class="alert alert-primary">
<p class="lead">Largest prime less or equal than <%= n %> is <%= prime %></p>
<p>Likes: <%= likes %></p>
<form method='POST'>
<input type="hidden" name="n" value="<%= n %>" />
<input type="submit" class="btn btn-primary" value="Like!" />
</form>
</div>
<% } %>
<!-- ... -->
The like is submitted via POST
request, and to parse its input we need
the body-parser
package:
$ npm install body-parser
We can now create a controller for the POST
route in app.js
and store the
posted like in a variable.
Storing likes in a variable is a bad idea. Each time the app restarts, it wipes all likes. We do this here only for convenience. In a production application, you should store such information in a database.
// ...
var bodyParser = require('body-parser');
.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app
// Like storage (in a serious app you should use a permanent storage like a database)
var likes = {}
.post('/', function (req, res) {
app.query.n] = (likes[req.query.n] || 0) + 1
likes[req.redirect('/?n=' + req.query.n)
res;
})
// ...
In addition, we also need to make sure the likes are passed to the render
function in the GET
controller:
// ...
// Render view with prime
.render('index', { n: req.query.n, prime: prime, likes: likes[req.query.n] || 0 });
res
// ...
To illustrate the problem with changing pages, let’s commit our current implementation and test it:
$ git commit -am 'Add view caching'
$ git push
$ ssh linzjax@<DROPLET_IP>
linzjax@express-memcache:~$ cd examples-express
linzjax@express-memcache:~/examples-express$ git pull origin master
linzjax@express-memcache:~/examples-express$ pm2 restart app.js
linzjax@express-memcache:~/examples-express$ pm2 log
If you submit a number, you will now get the largest prime below it, together with a Like button. However, when you click Like!, the like count doesn’t increase. This is because the view is cached.
To resolve this, we need to invalidate the cached view whenever it is updated:
// ...
.post('/', function (req, res) {
app.delete('_view_cache_/?n=' + req.body.n, function(err, val){/* handle error */});
mc.query.n] = (likes[req.query.n] || 0) + 1
likes[req.redirect('/?n=' + req.query.n)
res;
})
// ...
Deploy again to DigitalOcean using Github:
$ git commit -am 'Fix view caching'
$ git push
$ ssh linzjax@<DROPLET_IP>
linzjax@express-memcache:~$ cd examples-express
linzjax@express-memcache:~/examples-express$ git pull origin master
linzjax@express-memcache:~/examples-express$ pm2 restart app.js
linzjax@express-memcache:~/examples-express$ pm2 log
Now you can see the number of likes increase.
Session Caching
Memcache works well for storing information for short-lived sessions that time out. However, because Memcache is a cache and therefore not persistent, long-lived sessions are better suited to permanent storage options, such as your database.
To use sessions in Express, you need express-session
. To store the sessions
in Memcache, you need connect-memjs
:
$ npm install express-session connect-memjs
The configuration in app.js
is easy enough:
//...
var session = require('express-session');
var MemcachedStore = require('connect-memjs')(session);
// Session config
.use(session({
appsecret: 'ClydeIsASquirrel',
resave: 'false',
saveUninitialized: 'false',
store: new MemcachedStore({
servers: [process.env.MEMCACHIER_SERVERS],
prefix: '_session_'
});
}))
//...
Now you can now use sessions as you please. For more information about session usage in Express, check out the express-session documentation.
Clean up
Once you’re done with this tutorial and don’t want to use it anymore, you can clean up your DigitalOcean droplet instance by using:
$ doctl compute droplet delete express-memcache
This will clean up all of the DigitalOcean resources.