Secrets of the Ember-CLI server: Express middleware with Ember-CLI
Every Ember developer is familiar with the command to boot a webserver running their app:
ember serve
But did you know this server isn’t limited to serving Ember app assets? In fact ember serve
boots a Node.js Express server. Using the right hooks in Ember-CLI we can add new endpoints or behaviors to that server.
Why would we do this? Since running ember serve
in production is not recommended, we often use middleware with Ember-CLI for development mode niceties. In production, middleware can implement HTTP-level logic not needed in development.
Let’s take a look at what Express middleware is, how to use Express middleware with Ember-CLI, and how to deal with middleware in production when using Ember Fastboot.
Using Express
Express itself is distributed as an package on npm. Here is a simple Express program:
const express = require('express');
const app = express();app.get('/', function (req, res) {
res.send('Hello World!');
});app.listen(3000);
You can copy this program into a file serve.js
and run node serve.js
to start a server at http://localhost:3000.
The function passed to get()
is called a “middleware function” per the Express documentation. Middleware is just a term we use to describe the logic that handles an Express request. The world “middle” is used because middleware has a special characteristic: It can be combined with other middleware running before or after it.
When handling a request, middleware can choose to do one of the following with a request:
Send a response
When a piece of middleware responds, no middleware after it will execute. For example, the second piece of middleware in this application will never be called:
const express = require('express');
const app = express();app.get('/', function (req, res) {
res.send('Hello World!');
});app.get('/', function (req, res) {
throw new Error('This middleware will never be called!');
});app.listen(3000);
However middleware does not always respond. That brings us to a second way middleware can be combined:
Pass the request to the next piece of middleware
Middleware callbacks are passed a third argument usually named next
. next
can be called to pass the request to the next piece of middleware. For example, the first and second piece of middleware in this application are always called:
const express = require('express');
const app = express();app.get('/', function (req, res, next) {
console.log('Request made to: ', req.path);
next();
});app.get('/', function (req, res) {
res.send('Hello World!');
});app.listen(3000);
This code will log a message to the console then respond to the HTTP request with "Hello World!"
.
Middleware can use logic to choose between responding to a request or calling next()
. In the following example, the first piece of middleware will respond with an HTTP 500 based on the presence of a query param ?hello=false
. For any other query param value, it will pass the request to the second piece of middleware:
const express = require('express');
const app = express();app.get('/', function (req, res, next) {
if (req.query.hello === 'false') {
res.status(500).send('Hello request rejected');
} else {
next();
}
});app.get('/', function (req, res) {
res.send('Hello World!');
});app.listen(3000);
Modify the request, then pass it to the next piece of middleware
A third common use of middleware is to mutate a request before passing it to the next piece of middleware. For example, the first middleware in this code casts strings like 'false'
into boolean values:
const express = require('express');
const app = express();app.get('/', function (req, res, next) {
Object.keys(req.query).forEach(key => {
if (key === 'false') req.query[key] = false;
if (key === 'true') req.query[key] = true;
});
next();
});app.get('/', function (req, res) {
if (req.query.hello === false) {
res.status(500).send('Hello request rejected');
} else {
res.send('Hello World!');
}
});app.listen(3000);
The first middleware cleans up the req.query
values so the second middleware can treat them as booleans.
Express is a very flexible framework for writing HTTP servers, and of course can do much more than we’ve looked at here. the Middleware callback function examples section of the Express API reference is a good place to learn more.
Built-in Ember-CLI middleware
Ember-CLI comes with built-in middleware you can install on the command line. Mostly these are intended for stubbing XHR APIs or creating XHR endpoints that proxy to another server.
http-mock
In development you may want to experiment with a draft version of your API before writing the real thing. To do this you can generate an http-mock in an Ember app:
ember generate http-mock cats
This will generate a file server/mocks/cats.js
which you can use to implement your mock API. It will be added to Ember’s Express server, and can be requested at /api/cats/42
(for example) when running ember serve
.
Using this middleware is fairly rare. Instead, most Ember developers reach for Ember CLI Mirage. Mirage mocks endpoints and responses in the browser itself and provides an in-memory database of “server” records, making it a much more powerful tool for testing.
http-proxy
It is common for a production Ember app to host an API on the same domain as the Ember app assets. This avoids concerns like CORS, and allows your server to take full advantage of technologies like HTTP2.
To emulate a single-domain setup in development applications, Ember-CLI provides middleware for proxying from its Express app to another server. For example, you could generate a proxy from /api
to a local Rails server with the following command:
ember generate http-proxy api http://localhost:3000
Often you’ll need to edit the proxy middleware (for this example in server/proxies/api.js
) to have an accurate emulation of your application’s production environment.
These two tools solve for very specific use cases, but Ember-CLI also allows us to provide completely custom Express middleware.
Custom Express middleware with Ember-CLI
The Ember-CLI addon system allows npm packages to change the build pipeline of Ember applications. Addon’s don’t just provide runtime JavaScript you import
, they can compile SASS, strip whitespace from templates, or even add commands for deployment.
They can also provide middleware for the ember serve
Express server.
The easiest way to add custom middleware to an application is to use an in-repo addon. These are nominally npm packages, but instead of being published they remain in the same codebase as the application.
In an Ember application run:
ember generate in-repo-addon my-middleware
This generates a file lib/my-middleware/index.js
. This file is where the my-middleware
addon can change the application’s build or provide middleware.
Open this file and implement the serverMiddleware
hook:
/* eslint-env node */
'use strict';module.exports = {
name: 'my-middleware', serverMiddleware({app}) {
app.get('/hello', (req, res) => {
res.send('Hello world');
});
}
};
The serverMiddleware
hook is called by Ember-CLI with the Express app and some options. In the above example, we’ve added middleware that returns "Hello world"
when the browser visits /hello
.
Middleware order
You’ll recall that middleware has an order, and calling next
will pass a request to the next piece of middleware. Making sure your middleware is mounted in the correct order is a little tricky.
For example, the ember-cli-fastboot addon has extensive middleware. Fastboot handles all requests to its server, and will return its own 404 pages generated by an application. If you’re writing middleware for an application using Ember Fastboot, you will need to ensure your new middleware handles requests before ember-cli-fastboot.
To force an addon’s middleware to be ordered before or after another addon, use the ember-addon
property in the addon’s package.json
. For example, to make the in-repo addon my-middleware run before ember-cli-fastboot, open lib/my-middleware/package.json
and add the following:
{
"ember-addon": {
"before": "ember-cli-fastboot"
}
}
Read Configuring your ember-addon properties for more details about the options here.
Custom Express middleware in production with Ember Fastboot
Using Ember-CLI’s server in production (for example via ember serve -prod
) is not encouraged. Along with performance concerns, the Ember-CLI codebase is very large and complex. It is not designed for use in a production environment and may have security vulnerabilities when run as a public-facing server.
Instead, developers are encouraged to use the Ember Fastboot App Server. This package is not an addon, instead it is an Express application that can run your Ember app in the Fastboot environment based on production build assets.
To use the Ember Fastboot App Sever, first install it as a dependency:
yarn add fastboot-app-server
Then create a serve.js
file:
const FastBootAppServer = require('fastboot-app-server');let server = new FastBootAppServer({
distPath: 'dist'
});server.start();
Run the following commands to boot the server:
ember build -prod
node serve.js
Your Ember application is now running in Fastboot with a production-ready Express server.
The Fastboot Express server can be customized in several ways covered by the documentation. Let’s consider one common use case: In order to be a Progressive Web App, a website must redirect HTTP users to HTTPS. Heroku doesn’t do this by default, so if our Fastboot application is hosted there we will need to add the redirect in middleware. This is a good example of how some middleware is useful in development (proxies for example) and some is appropriate for production (protocol redirects).
Open serve.js
and implement the beforeMiddleware
hook:
const FastBootAppServer = require('fastboot-app-server');let server = new FastBootAppServer({
distPath: 'dist',
beforeMiddleware(app) {
app.use((request, response, next) => {
if (request.headers['x-forwarded-proto'] === 'https') {
next();
} else {
response.redirect(
301, `https://${request.hostname}${request.url}`
);
}
});
}
});server.start();
Your server now redirects HTTP users to HTTPS. At this point, we’re writing middleware code that is completely disconnected from Ember-CLI, so there is nothing to stop us from using the full capability of Express.
Examples
If you’re trying to write Express middleware for your own Ember application or addons, these projects are good references:
- 201-created/ember-hexo-blog is an Ember-CLI addon that implements middleware for serving static assets at a path and orders itself before ember-cli-fastboot.
- 201-created/bodega is an Ember app that implements HTTPS redirect middleware on top of the Ember Fastboot App Server.
At 201 Created we’ve helped dozens of clients (including YC startups and Fortune 50 companies) make the most of Ember-CLI, the Ember ecosystem, and their own production deployments. Contact us to learn more about our work and how we can make your team more effective.