Building a Minimum Viable Web App with React, Express JS and Docker

Matt Fowler
Coding With Clarity
11 min readFeb 12, 2019

Building a secure web app with user creation and login

React and Node are powerful tools for building web applications quickly. In this article we’ll see how to create an end to end web application using these tools and tie it all together with Docker.

We’ll build an application which will allow a user to create an account, login and see a welcome message. To do this we’ll see how to:

  • Set up a data model in MySql and run database migrations with Flyway.
  • Create a REST API with Express JS and Sequilize.
  • Secure a REST API with JWTs.
  • Use React and React Router to create a single page frontend.
  • Tie everything together with Docker Compose for a one command and running development environment.

Complete code for the above is available at:

Getting Started with a Data Model

The first thing we’ll need to do is create a user table in our database. We’ll store a few basic things in this table: a unique id, a username, a password and we’ll also track creation date. Our simple schema will look like this:

CREATE TABLE mvw_user (
id int NOT NULL AUTO_INCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (id)
);

Later we’ll see how to apply this to our database with a migration tool called Flyway.

Creating the Data Model in Node with Sequilize

Now that we have a data model, we’ll want to start using it in code. For this, we’ll use an ORM called Sequilize.

First we’ll want to create a connection to our database:

We’re doing a few things in this code snippet:

  1. We’re using process.env to pass in information about where our database lives and what the password is via environment variables.
  2. We’re creating a new instance of Sequilize with the connection info and a connection pool. A connection pool will help our application’s performance by reusing database connections.

Building a Sequilize User Model

Next, we’ll use our connection to define our Sequilize model. Once we do this, we’ll be able to use the methods that Sequlize provides to query our database without writing any SQL ourselves. Our model looks like this:

What we’re doing here is mapping our database fields to a JS object and defining their types. The interesting bit here is the timestamps section. Sequilize allows us to define timestamp fields, which means the framework will keep timestamps up to date for us when a record is created or updated.

With this we can import User wherever we need and call methods like User.findById to find a user with a specific id.

Adding an Endpoint to Create Users

We can now use our Sequilize model to create users. To do this, we’ll want to create a REST endpoint that accepts a username and password as JSON like so:

POST /users {username: 'username', password: 'password'}

We’ll use Express — a popular JS web framework to do this. Express allows us to define endpoints with routing. We can simply define a path and pass in a callback with the code we want to execute when we visit that path:

Here we define a POST endpoint at / with a callback that does nothing. Each Express router callback has 3 arguments:

  1. request — the request we received from the client.
  2. response — the response we’ll send back to the client.
  3. next — In Express we can chain router callbacks together to build more complex endpoints that reuse code. This comes in handy for cross-cutting concerns like error handling and authentication. next is a special function that we can use to call the next callback in the chain. We’ll see how to use this shortly.

So now let’s implement our user creation. We’ll want our endpoint to do the following:

  1. Create a user if one does not already exist with the provided username.
  2. Respond with an error if someone is already using the given username.
  3. Respond with an error if the request does not have a username or password.

We can create a user fairly easily with Sequilize’s create method by passing in the username and password. The create method returns a promise. This means that we’ll need to call then on the return value to process a successful creation, and use catch to handle any errors that may occur:

If we successfully created a user, we respond with a 201 status code and a JSON message that the user was created. If there was an error, we catch it and call the next function with the error to let our error handler process the failure (we will write this error handler later on).

Note that we use a bcrypt library to encrypt the password, as it is bad security practice to store plaintext passwords in our database.

To handle the case where the user already exists, we’ll wrap our creation code in a Sequilize findOne call and search for a user with the proposed username. If we find a user we’ll respond with a 400, otherwise we’ll create a user.

To handle the case where we have a missing username or password on the request, we could just put the code directly in our router method. However, we’ll want to reuse this for our login code, so we’ll abstract it out instead. To do this we can use middleware which will let us reuse this code among multiple endpoints. A middleware function looks exactly like a router callback and we write it in the same way.

In this function we check to see if we have both a username and password in the request. If we don’t have a username and password, we’ll send a bad request status code back to the client, otherwise we call the next function which will call the next route in the chain, in this case our endpoint’s callback.

Now we can embed this method right after the path in our router. When we call this route our ValidateUsernamePassword middleware will get evaluated first before evaluating our main callback:

With this we’ve satisfied all the requirements of our account creation endpoint. We’ll save this code in a file called users.js and now we can create the main entry point for our application, app.js

There’s a few things going on here:

  1. We create an Express app by calling express()
  2. We tell Express to use its JSON parsing middleware with app.use(express.json())
  3. We import our users.js router code and tell Express to use it. This will send every request that starts with /users to our usersRouter.
  4. We create middleware that will respond with a 404 if an incoming request doesn’t match any routers.
  5. We create middleware that will handle any exceptions we get and respond with a JSON error message.
  6. We tell the application to listen on port 8000, this starts our application.

We can start the application by simply running node app.js provided we have the proper environment variables set.

Now we can test our endpoint out with Curl:

Login and Securing Endpoints with JWTs

Next, we’ll need an endpoint to allow our users to log in and get an access token. We’ll also want to build another endpoint to get a user’s data, which we need to secure against unauthorized access.

To do both of the above, we’ll use JSON Web Tokens. JWTs work by taking an unencrypted JSON payload and signing it with a secret. This produces a long token that we can then pass along on subsequent requests. When our API receives a token, we can decode it and get our original payload back. If a token is invalid or is tampered with, we won’t be able to decode it and we won’t let the request go through, thus securing any endpoint we put this check on.

Our login endpoint will be a POST method that takes a username and password much like our user creation endpoint. We’ll check to see if a user exists and if their password is correct. If so we’ll create a JWT to be used on subsequent requests.

jwt.sign creates a JWT with a payload of {userId: user.id}. We’ll be able to get the userId back out later by decoding the token. We also specify expiresIn, which will automatically invalidate the token after the specified time, in this case, one week.

Next, we need to secure our endpoints by validating that the JWT in the Authorization header are valid. To accomplish this we’ll build some middleware we can embed in any route we want:

If a token is successfully validated with jwt.verify, we add the userId from our token to our request object for future routes to use and call next. This will come in handy four our next endpoint which will use the user id to look up the currently logged in user.

Building an Endpoint to Get a User’s Data

Next we’ll define a GET endpoint,/users/me, which will respond with the currently logged in user’s username and their created date. We’ll use our ValidateJWT middleware to ensure we have a valid logged in user. Since this middleware embeds the userId on the request, it is relatively trivial to look up the user and then respond with their data:

Given this endpoint, we now have everything we need to build a frontend with React.

Building a UI With React

We’ll be using a few technologies to help us build our react SPA:

  • Axios — This will handle sending requests to our API
  • React Router — This will help us switch and redirect between components.
  • Bootstrap — A CSS library for styling our application, we won’t get too much into this.

First, we’ll set up our index.html page — this is the application’s “single page.” We’ll also create a “root” element that we’ll tell React to use later on; this is the div where the application will get rendered.

Next, we’ll write index.js, which will take the root element and render the App component (our top level component, which we need to implement) inside of it.

We also wrap our app in React Router’s HashRouter. This router puts a hash after the URL and will take everything after that to determine the view that is rendered.

Now we’ll build our App component, this will act as a holder for all of our other components. We’ll stub out the components we want to use here and implement them later, we’ll need three:

  1. Login — Handles logging into the application.
  2. Create Account — Handles creating our accounts.
  3. Home — Displays a welcome message to the user.

In our render method we useSwitch. This defines the different components we can transition between. Then for each component we set up a Route and specify a path. When we go to that path, the router will render that component.

Now we can start implementing the components we’ve stubbed above. Let’s start with Login.

Building a Login Component

React works by tracking a component’s state. As developers we are in charge of keeping that state up to date, so we’ll need to set some sensible defaults in our component’s constructor. We’ll set our username and password to an empty string and an error message to undefined because we have no errors yet. We’ll also add a loading flag to determine if we should show a loading spinner.

Now we’ll want to render our username and password fields and keep track of the changes when a user enters text into them. To do this, we’ll create a handleChange method which will take the id of a given target and uses it to update its state.

Since on each input we’ve specified an id that matches a key in our state, whenever the username or password input is changed the component’s state automatically gets updated. We also create an a errorMessageDiv and a loadingDiv that only gets displayed if we have an error message or if we’re currently loading.

We’ve also added a login button and method we still need to implement, but how do we make requests to our API so that login works?

To actually handle the requests to the API, we’ll create a UserService helper class. This class will handle sending the data and will allow us to pass in callbacks that handle the success and failure of the call.

We’ll also need somewhere to store the tokens our login call gives us. To do this, we’ll create a helper class called AuthStore that will store our tokens in our browser’s local storage. We’ll also set the token on the Axios headers, which ensures that when we make any API calls we will pass the token along.

Now we can use this to implement our login function:

Our login method is pretty easy to follow: we set the state to loading and call the login method. In handleLoginResponse: if we get a token, we use AuthStore to save it and then use React Router to redirect the user to the home component with this.props.history("/"), otherwise, we show an error message. In handleLoginError: we handle the case where the server returned an error and simply set loading to false and show a generic error message.

Now that we’ve built login, let’s build our Home component.

Building a Home Component

Right when we load the component we’ll want to make a call to loadCurrentUser so it can show the user data. We’ll put this call in componentDidMount — this is where we want to make API calls when a component first loads. We set loading to true in our constructor because we are making an API call when the component first loads.

Our success and error callbacks for loading a user are similar to the ones in our login component:

Our render method is similar to login as well. If we’re loading, we display a spinner, otherwise we display a the welcome (or error) message to the user.

Note that we don’t want a user who is not logged in to see this component. We can actually create our own type of route for this. We’ll do this in our App component:

Before, we wrapped our Home component in a Route, but now we wrap it in our custom PrivateRoute. Now whenever someone tries to go to the Home component, they’ll automatically get redirected to the login screen if they aren’t logged in. If they are logged in, we’ll just use the component normally.

For the sake of brevity, we’ll skip over the account creation component. Instead, we’ll see how to use Docker to get a development environment to run in one command.

Using Docker

Docker is a technology that allows us to build containers for our software. Think of a container as a standardized way to bring up an application with all its needed components. The basic way to do this is by writing a Dockerfile, which will build an image that we can then deploy in a container.

First let’s write a Dockerfile for our API:

We use a base image, node:11-alpine, which has the basic software we need to start the application, in this case Node. Then, we set some environment variables for our application, copy our package.json with our dependencies, install them, and launch our application with node app.js

For our UI, we’ll build a very similar Dockerfile to our API, the only thing much different is our start command.

Now we’ll use Docker Compose to start these containers up alongside one another. We’ll also start up a database and run the SQL we created earlier against it with Flyway.

Using this we can run docker-compose up and a few things will happen for us:

  1. We’ll get an instance of mysql running.
  2. We’ll use Flyway to connect to the database and run our migrations located in our sql directory.
  3. We’ll start up our API and our UI

Now if we go to http://localhost:3000 we should be able to see the complete application running.

Source Code

There’s a lot to unpack here and we couldn’t cover everything in great detail in this article. Please take a look at the source code on Github and play around with it or use it to build your own application!

--

--

Matt Fowler
Coding With Clarity

Boston based full stack polyglot software architect specializing in JVM languages, iOS and agile development.