Create a React application from scratch (Part 5): Setting up an Express Server

Mostafa Fouad
14 min readJan 11, 2018

--

This post is part of a beginner-level series of posts meant for people who use
ready-made tools, templates or boilerplates for React but wish to learn and understand how to build a React application from the very beginning.

All posts in this series:
Part 1: Introduction
Part 2: Initialization and the First File
Part 3: Using ES2015 Syntax
Part 4: Enforcing a Style Guide
Part 5: Setting up an Express Server
Part 6: Using a Module Bundler
Part 7: Setting up React and Best Practices
Part 8: Setting up Redux
Part 9: Setting up React Router
Part 10: TDD and Setting up Jest

Your React application might be purely front-end, but what if it is not? What if you need a back-end? Maybe make some sensitive API calls? How about handling and validating form submissions?

Express.js, or just Express, is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

You can easily create an application skeleton using the Express Generator tool but since we are here to learn, we are going to keep building from scratch.

Note: We are going to create the application almost exactly the same way Express Generator creates it, so you can skip this section and use the generator if you want to do so.

Simple structure

We are going to start with a very simple setup and gradually build on it so that you can understand exactly what the Express Generator adds to your application. Install Express from npm as a dependency:

$ npm install --save express

Inside index.js file, delete the sample code that we have right now and import express:

import express from 'express';// Express app setup
const app = express();

To create a server, you would need the http module from Node.js core:

import http from 'http';
import express from 'express';
// Express app setup
const app = express();
const server = http.createServer(app);
server.listen(3000);
server.on('listening', () => {
console.log('Server is listening on port: 3000');
});

The http.createServer() method accepts a request listener (in this case, it is app which is essentially just a function) and returns a server instance. The method server.listen() starts the server on the specified port and then we simply log a message to the console once the server starts listening.

If you execute npm start to run the application now, the server would start on port 3000 but it will not be able to handle any requests. If you try to navigate to http://localhost:3000/, you would get a generic404 error.

Let us create a route handler to handle any request and print the message ‘Hello Express’. Append the following code to index.js file:

app.get('*', (req, res) => {
res.end('Hello Express');
});

The app.get() method is a route method for handling GET requests made to the server. We are stating that we will accept any GET request (that is what the asterisk means) and respond with the string ‘Hello Express’.

Restart the application and try to navigate to http://localhost:3000/ again. This time you will see ‘Hello Express’ printed. If you try to navigate to any other url (for example: http://localhost:3000/woohoo/) under the same domain, you will see the same message.

Too simple to scale

While this setup of a single index.js file works, it is less than ideal and is not scalable nor maintainable. You would not have a single route handler, you might have so many routes that you would need a router object to handle your application endpoints.

You also would not render your application views as strings but instead you would use a template engine and you should also have a server file that handles starting the application and handles different server errors.

Better structure

The Express Generator tool creates a cleaner architecture for your application. To follow the same architecture we are going to create 4 new directories:

  • /server which would contain the server file that starts the application.
  • /routes which would contain all route handlers for the application.
  • /views which would contain the templates, we will use Pug as an engine.
  • /public which would contain all public static files such as images, font files, stylesheets, minified scripts and any other files that can be accessed publicly.

The server file

Let us start off with the server file. Create a new file inside server directory and name it index.js. This file will be the new entry point of the application. It will be responsible for starting the server and handling any errors that may arise during the process.

This would be the code that goes into the server/index.js file:

/**
* Module dependencies.
*/
import http from 'http';
import express from 'express';
/**
* Express app setup
*/
const app = express();/**
* Simple logger function.
*/
function log(message) {
process.stdout.write(`${message}\n`);
}
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10);
if (Number.isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || 3000);
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
let availablePort = port;
/**
* Listen on provided port, on all network interfaces.
*/
function startServer(serverPort) {
server.listen(serverPort);
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = `${
typeof port === 'string' ? 'Pipe' : 'Port'
} ${port}`;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
log(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
if (availablePort - port < 10) {
availablePort += 1;
startServer(availablePort);
} else {
log(`${bind} is already in use`);
process.exit(1);
}
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address();
const bind = `${
typeof addr === 'string' ? 'pipe' : 'port'
} ${
typeof addr === 'string' ? addr : addr.port
}`;
log(`Server is listening on ${bind}`);
log(`Visit: http://localhost:${addr.port}`);
}
/**
* Start server.
*/
server.on('error', onError);
server.on('listening', onListening);
startServer(availablePort);

I know that this may seem like a lot of code but do not get overwhelmed. The inline comments explain what each part of the code does but let us break it down to make things more clear:

  • The log(message) function simply logs a message to the terminal using the standard output stream.
  • The normalizePort(val) function simply normalizes a port into a number, string, or false.
  • The startServer(serverPort) function starts the server using the specified port number.
  • The onError listener function handles server errors but most interestingly is that it would handle a busy-port-error by checking if the port is already in use and would try to restart the application on a different port.
    So for example, if you try to start the app on port 3000 but that port is already taken, it would try to restart the app on port 3001. If that fails as well, it would try again with port 3002 and so on.
  • Finally, the onListening listener function just logs a message to the terminal as a notification that the server is running and the application has successfully started.

Now change the ‘start’ script in package.json to point to /server/index.js file:

{
...
"start": "babel-node ./server/index.js",
...
}

Once you have updated the package.json file, restart the application to see the changes.

Side note

There is a slight difference between what we have done and what the Express Generator tool does. When you create an Express app using the express CLI command, the server file is created as a binary in /bin/www and the first line of the file is a shebang:

#!/usr/bin/env node

This line simply tells the system to use Node to run the server file. This is useful if your application is intended for global installation and usage. You would specify the binary file in package.json as follows:

...
"bin": {
"myapp": "./bin/www"
}
...

Then every time your application is installed globally:

$ npm install -g myapp

It can be used as a normal command:

$ myapp

This also allows npx to find your binaries easily when your application or module is installed locally.

Creating the app.js file

Currently, the only file that has a reference to the Express app instance is the server file. We will need to add different middle-wares to handle body parsing, routing, logging and other things, so we need to be able to import this app instance.

We can do this by moving the instantiation of app to a different file on the root directory of the project:

/**
* app.js
*/
import express from 'express';// Express app setup
const app = express();
export default app;

Then import that instance back inside the server file:

/**
* Module dependencies.
*/
import http from 'http';
import app from '../app'; // <-- import app
/**
* Simple logger function.
*/
function log(message) {
process.stdout.write(`${message}\n`);
}
...

Important Note:

In order for your code changes to take place, you would have to restart the application each time you edit and save a JavaScript file that is related to the server. To avoid restarting the application manually, you can use Nodemon.

Nodemon watches the files in a directory, and if any files change, it will automatically restart the application. Install it as a development dependency:

$ npm install --save-dev nodemon

Then add a dev script to the package.json file:

{
...
"dev": "nodemon --exec babel-node ./server/index.js"
...
}

Now each time you want to start the application for development, use npm run dev instead of npm start and when you make changes to the server code, Nodemon will restart the application for you.

Creating routes and handling requests

If you start the server now and try to access the application, you should get the same generic 404 error that you had earlier. Again, this is simply because we do not have any route handlers that respond to requests made to the server.

To fix this problem, we are going to use Express Router. Create an index.js file inside the routes directory:

import express from 'express';const router = express.Router();/* GET home page. */
router.get('*', (req, res) => {
res.render('index');
});
export default router;

In this file, we create a router instance that handles any requests (that is what the asterisk means) made to the application by responding with a single view. This view file still does not exist but we are going to create it in a second.

Go back to the app.js file and modify the code to use the routes we have just created:

import express from 'express';import routes from './routes';// Express app setup
const app = express();
// use routes
app.use('/', routes);
export default app;

What you should note here is the usage of app.use() to mount our routes to the root path of the application.

Setting up the view engine

Next, we will specify the location of our views and the view engine that Express should use:

import path from 'path';
import express from 'express';
import routes from './routes/index';// Express app setup
const app = express();
// view engine
app.set('views', path.join(__dirname, './views'));
app.set('view engine', 'pug');
// use routes
app.use('/', routes);
export default app;

We are going to use Pug (previously known as Jade) as a view engine, so let us install it from npm as a dependency:

$ npm install --save pug

Now create the index.pug view file inside the views directory:

doctype html
html
head
title Hello Express
body
h1 Hello Express

Navigate to http://localhost:3000/ again and you will see a large message that says ‘Hello Express’. This is a very basic page, a normal page would probably contain more tags in the document head and body for the keywords, stylesheets, scripts … etc.

doctype html
html
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1, minimum-scale=1")
meta(name="description", content="")
meta(name="keywords", content="")
title= title link(rel="shortcut icon", href="/img/favicon.ico") body
h1 Hello Express

Template Inheritance

If you have another page that would be rendered for another route, you would have to copy/paste the same tags to the new page. Fortunately, Pug offers a way to handle such cases properly, it is called Template Inheritance.

Create a layout.pug file:

doctype html
html
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1, minimum-scale=1")
meta(name="description", content="")
meta(name="keywords", content="")
title= title link(rel="shortcut icon", href="/img/favicon.ico") body
block content

Then simplify index.pug to the following:

extends layoutblock content
h1 Hello Express

Voilà! Now you can create as many pages as you need that simply extend this layout.

Handling server errors

With our current setup so far, the router will handle all requests by responding with the index.pug view. But what happens if a server error occurs while responding? Modify the route handler to throw an intentional error:

router.get('*', (req, res) => {
throw new Error('Oops');
res.render('index');
});

As you can see, we have a really ugly error page:

An ugly error page

Let us do something about this, modify app.js file with the following code:

...// error handlers
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.render('error');
next();
});
export default app;

As you may have already noticed, we used app.use() again, but this time we used it without specifying a route and the function signature contains four parameters. This is important, you must provide four arguments for Express to identify the function as an error-handling middleware function.

Even if you do not need to use the next object, you must specify it to maintain the four-parameters signature. Otherwise, the function will be interpreted as regular middleware and will fail to handle errors.

Now let us create our error page /views/error.pug:

extends layoutblock content
h1 This is an error
p Something went terribly wrong

This is how the error page would look like:

A not so ugly error page?

I know that this error page is not a Picasso painting or anything but at least the stack trace is no longer displayed. Now, you might be wondering if it is a good thing or a bad thing to hide the error stack trace … and to answer your question: ‘It depends’.

You want to see the stack trace while you are developing and writing code but you definitely do not want any of that displayed to your users on production.

Let us modify our code to hide the stack trace on production and keep it on development environment:

// error handlers
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: app.get('env') === 'development' ? err : {},
}
);
next();
});
export default app;

The error page template becomes:

extends layoutblock content
h1= message
h2= error.status
pre #{error.stack}

Note: Do not forget to delete the line that throws an intentional Oops error in the routes file: routes/index.js

Handling 404 errors

To handle 404 errors and route to our custom error page, we are going to create a middleware function that can handle any request.

// use routes
app.use('/', routes); // <--- error 404 handler comes after this
// catch 404 and forward to error handler
app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: app.get('env') === 'development' ? err : {},
});
next();
});
export default app;

Placing the middleware function after all other route handlers is important. If the requested route can be handled by the application, one of the route handlers will process it, otherwise the error 404 handler will receive the request and render our error page.

Creating a public directory

The application will only handle routes you have defined inside your routes file, but we are going to use a lot of static assets in our application such as images, fonts, videos, stylesheets, scripts … etc.

It would not make any sense to create a new route for each asset we want to use, so we are going to create a single directory where we can serve these static assets. We are going to name this directory: public.

Express provides a very handy built-in middleware function that allows you to specify a root directory from which to serve static assets.

Modify app.js file:

// serve static files from 'public'
app.use(express.static(path.join(__dirname, './public')));
// use routes
app.use('/', routes);

Reading environment variables

Storing configuration in the environment separate from code is based on The Twelve-Factor App methodology. To achieve this, we are going to use a package called Dotenv.

Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env.

Install Dotenv as a dependency:

$ npm install dotenv --save

Then use Dotenv as early as possible in your code:

import dotenv from 'dotenv';// use dotenv
dotenv.config({
silent: true,
});
...

Then create the .env and.env.example files on the root directory of the application.

Note: You should commit and push your .env.example file but not .env file.

A couple more things

Right now, the application receives requests and responds to them without any trace of the requests or any errors that may have occured while responding. You need to keep a history of these requests and errors by logging them so that you can check them later when you run into a problem on production — yes, it is ‘when’, not ‘if’.

Morgan is a popular HTTP request logger middleware function for Node. It is the default logger used by the Express Generator.

Install the package as a dependency:

$ npm install --save morgan

Then use the middleware function in app.js file:

import logger from 'morgan';// logger
app.use(logger('combined'));

Two other useful packages utilised by the Express Generator are Body Parser for parsing incoming request bodies and Cookie Parser for parsing the cookie header and populating the request with an object keyed by the cookie names.

Install both of them as dependencies:

$ npm install --save body-parser cookie-parser

Then use them:

// body parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// cookie parser
app.use(cookieParser());

Finally…

We have done lots of changes to app.js and you might be confused or lost, so here is how the complete file should look like:

/**
* app.js
*/
import path from 'path';
import express from 'express';
import dotenv from 'dotenv';
import logger from 'morgan';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import routes from './routes';// use dotenv
dotenv.config({
silent: true,
});
// Express app setup
const app = express();
// view engine
app.set('views', path.join(__dirname, './views'));
app.set('view engine', 'pug');
// logger
app.use(logger('combined'));
// body parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// cookie parser
app.use(cookieParser());
// serve static files from 'public'
app.use(express.static(path.join(__dirname, './public')));
// use routes
app.use('/', routes);
// catch 404 and forward to error handler
app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: app.get('env') === 'development' ? err : {},
});
next();
});
export default app;
Directory structure should look like this by now

Conclusion

Your application could be a simple as a single HTML file, but usually it is not. Express is a popular Node.js web application framework and we will use it to create our development and production servers and to handle server-side routing as well.

Was this article useful? Please click the Clap button below, or follow me for more.

Thanks for reading! If you have any feedback, leave a comment below.

Go to Part 6: Using a Module Bundler

--

--