Contract-first development: Create a mock back end for realistic data interactions with React

Contract-first development: Create a mock back end for realistic data interactions with React

Many front-end developers are discovering the benefits of contract-first development. With this approach, front- and back-end developers use OpenAPI to collaboratively design an API specification. Once the initial specification is done, front-end developers can use API definitions and sample data to develop discrete user interface (UI) components. Defining a single OpenAPI spec improves cross-team collaboration, and API definitions empower front-end developers to design our initial workflows without relying on the back end.

Still, we eventually need to verify our assumptions about the application workflows against real data. This is where the challenge comes in. Enterprise security policy typically prevents cross-origin resource sharing (CORS), so our data requests will be rejected by the browser. What we need is a dependable way to make changes without updates to the back-end security policy.

In this article, I will show you how to use React.js and a few simple configurations to create a fake back end, which you can use to test your front end with realistic data interactions. I’ll also show you how to switch your application config from the fake back end to a development environment, and how to work around a CORS error that pops up the first time you make that switch.

Authenticating users with a fake back end

Most single-page applications (SPAs) are developed with multiple user roles in mind, so front-end developers design our workflows for different types of users. It can be difficult to add test users to Active Directory or a corporate LDAP system, however, which makes testing user roles and workflows in an enterprise environment especially challenging.

I’ll introduce a three-headed configuration for running your application through local, dev, and production modes. You’ll create a fake back end, integrate it with a router, and test a variety of user roles and workflows against it. The back end will run on your local development machine.

Everything you need to grow your career.

With your free Red Hat Developer program membership, unlock our library of cheat sheets and ebooks on next-generation application development.

SIGN UP

Step 1: Configure a fake back end for local development

To start, take a look at the example JavaScript in Listing 1. Note that this configuration is separate from the authentication mechanism used in production:

import { fakeAuth } from './helpers/fake-auth';
import configureSSO from './helpers/sso';

const dev = {
  init: () => {},
 auth: fakeAuth,
 useSSO: false,
 apiUrl: '',
};

const prod = {
 init: () => {
   configureSSO();
 },
 auth: null,
 useSSO: true,
 apiUrl: 'https://production.example.com',
};

const config = process.env.REACT_APP_STAGE === 'production' ? prod : dev;

export default {
 TITLE: 'My Fabulous App',
 ...config
};

Listing 1. Config for a fake back end (src/config.js).

Note that the const prod object contains a function call for init, which sets up authentication using single sign-on (SSO). To avoid multiple initializations, be sure to reference auth in only one place in the application. Also, notice that you can use the export default configuration at the bottom of the script to manage common key/value pairs.

Step 2: Write a fake authentication script

For the fake authentication, we start with a list of mocked-up users configured for a variety of roles. As you can see in Listing 2, the user with the email lreed@vu.com has the admin role, whereas the others are normal users:

export function fakeAuth(url, options) {
 let users = [
   {
     id: 0,
     email: 'lreed@vu.com',
     name: 'Lou Reed',
     password: '123',
     role: 'admin'
   },
   {
     id: 1,
     email: 'jcale@vu.com',
     name: 'John Cale',
     password: '123',
     role: 'user'
   },
   {
     id: 2,
     email: 'smorrison@vu.com',
     password: '123',
     name: 'Sterling Morrison',
     role: 'user'
   }
 ];

 return new Promise((resolve, reject) => {
   // authenticate - public
   if (url.endsWith('/login') && options.method === 'POST') {
     const params = JSON.parse(options.body);
     const user = users.find(
       x => x.email === params.email && x.password === params.password
     );
     if (!user) return error('Username or password is incorrect');
     return ok({
       email: user.email,
       role: user.role,
       name: user.name,
       token: `fake-jwt-token.${user.role}`
     });
   }

   // private helper functions
   function ok(body) {
     resolve({
       ok: true,
       text: () => Promise.resolve(JSON.stringify(body))
     });
   }

   function error(message) {
     resolve({
       status: 400,
       text: () => Promise.resolve(JSON.stringify({ message }))
     });
   }
 });

}

Listing 2. A fake authentication script (src/helpers/fake-auth.js).

Notice that the export function behaves like window.fetch does for a POST request. This will make the fake back end easy to replace with a real back end that behaves the same way.

The rest of the script is easy to follow. If we find the matching user by email and password, we return it. Otherwise, we return a 400, indicating the email or password was incorrect. We will only call the fakeAuth() method for login attempts, so we don’t need to do anything fancy like proxying all requests through this method.

Next, we want to ensure that we can utilize the authentication mechanism and expose the current user to our application.

Step 3: Write a minimal UserService

In Listing 3, we use an ECMAScript 6 class to create the UserService. We can inject this service into our components as a property, and it will be deployed to inspect the current user. Designing the service this way also makes it easier to encapsulate its functionality for the application’s LoginPage:

import { BehaviorSubject } from 'rxjs';

class UserService {
 constructor(back end) {
   this.back end = back end;
   this.currentUserSubject = new BehaviorSubject(
     JSON.parse(localStorage.getItem('currentUser'))
   );
   this.currentUser = this.currentUserSubject.asObservable();
   window.addEventListener('storage', this._listenForStorageChanges);
 }

 _listenForStorageChanges = (win, event) => {
   const nextUser = JSON.parse(localStorage.getItem('currentUser'));
   if (nextUser !== this.currentUserSubject.value) {
     this.currentUserSubject.next(nextUser);
   }
 }

 login(email, password) {
   const requestOptions = {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify({ email, password })
   };

   return this.back end('/login', requestOptions)
     .then(this._handleResponse)
     .then(user => {
       localStorage.setItem('currentUser', JSON.stringify(user));
       this.currentUserSubject.next(user);

       return user;
     });
 }

 logout() {
   localStorage.removeItem('currentUser');
   this.currentUserSubject.next(null);
 }

 get currentUserValue() {
   return this.currentUserSubject.value;
 }

 _handleResponse(response) {
   return response.text().then(text => {
     const data = text && JSON.parse(text);
     if (!response.ok) {
       if ([401, 403].indexOf(response.status) !== -1) {
         this.logout();
         window.location.reload(true);
       }

       const error = (data && data.message) || response.statusText;
       return Promise.reject(error);
     }

     return data;
   });
 }
}

export default UserService;

Listing 3. A minimal UserService (src/services/UserService.js).

The UserService class uses dependency injection to pass in the back end. Later, we’ll be able to substitute the correct back-end auth for our mock configuration. Notice, also, that we inspect the user in local storage upon construction. This allows an SSO implementation like Keycloak to ensure that a user is set upon application entry. The logout() method simply removes the user from local storage and clears the BehaviorSubject.

Step 4: Set the entry point for configuration (index.js)

The root of the application is hosted in index.js, so it’s important that we use this file as the configuration’s entry point. Listing 4 shows this config:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import config from './config';

ReactDOM.render(
&lt:App title={config.TITLE} ssoEnabled={config.useSSO} auth={config.auth} />,
 document.getElementById('root')
);

Listing 4. Set index.js as the configuration’s entry point (src/index.js).

Notice that we also pass an auth to the application, along with a flag declaring whether or not we are using SSO. We need this flag because SSO disables the login page, which is required for local development.

Using the React.js router to control access

Once we have a way to authenticate users, we can configure the React.js router to control what’s visible based on each user’s authorization.

In Listing 5, we configure App.js so that we can observe whether or not a user is logged in:

import React, { Component } from 'react';
… // imports hidden for brevity

class App extends Component {
 constructor(props) {
   super(props);
   this.state = {
     currentUser: null
   };
   this.userService = new UserService(props.auth);
 }

 componentDidMount() {
   this.userService.currentUser.subscribe(x =>
     this.setState({
       currentUser: x
     })
   );
   if (!this.state.currentUser && !this.props.sso) {
     history.push('/login');
   }
 }

 render() {
   const { currentUser } = this.state;

   return (
     <Container fluid={true}>
       <Heading history={history} userService={this.userService} />
       <Router history={history}>
         {!currentUser && !this.props.sso && (
           <Route path="/login" render={props => (
               <LoginPage history={history} userService={this.userService} />
             )}
           />
         )}
         {currentUser && (
           <Route path="/" render={props => (
               <MainContent {...props} user={this.state.currentUser} />
             )}
           />
         )}
       </Router>
     </Container>
   );
 }
}

export default App;

Listing 5. Configure the application to use the React.js router (src/App.js).

Note how we’re using the UserService class in componentDidMount to subscribe to currentUser‘s state. We need that information to show users different pages based on their authorization. We’ll also be able to pass currentUser down to various child components, or perhaps make the user available via our React context.

Next, we’ll work on the local configuration for our fake back end.

Introducing a local configuration

We’re now ready to set up a fake back end that we can use locally to serve up data. We want the front end to behave as if it’s talking to a real back end, so that we can ensure that we don’t have static data lingering in our application. We’ll use a package called json-server for our fake back end. So that we don’t have to clone a separate project, we’ll just create a subfolder in the main project, called fake-back end.

Step 1: Create a fake back end in the local environment

In the fake-back end directory, use npm init to create a skeleton package.json. Edit this file and add the following start script to the scripts section:

 "scripts": {
   "start": "json-server -p 3007 -w db.json",
   "test": "echo Error: no test specified && exit 1"
 },

Listing 6. A start script for json-server (fake-back end/package.json snippet).

We need to be able to run the json-server command from the command line, so we’ll install it globally. Use the following command:

$ npm i -g json-server

Next, we need to create a set of data on which json-server will operate. Create the file shown in Listing 7 in the fake-back end folder:

{
   "catalog": [
       {
           "id": 0,
           "title": "The Velvet Underground & Nico",
           "year": 1967,
           "label": "Polydor",
           "rating": 5.0
       },
       {
           "id": 1,
           "title": "White Light/White Heat",
           "year": 1968,
           "label": "Polydor/Verve",
           "rating": 5.0
       }
   ]
}

Listing 7. A mock data set for json-server (fake-back end/db.json).

This is a very simple database, but it works for our needs. Next, we’ll have our catalog service fetch data for us.

Step 2: Create the catalog service

Listing 8 shows CatalogService calling axios to fetch a list of albums:

import axios from 'axios';
import config from '../config';

export const getAlbums = async() => {
   const albums = await axios.get(`${config.apiUrl}/catalog`);
   return albums.data;
}

Listing 8. CatalogService calls axios for a list of albums (src/services/CatalogService.js).

Using async/await simplifies the logic shown in Listing 9, and you can see that we are not handling any errors. With this and an adjustment to the config, we can see that our fake back end is working:

import { fakeAuth } from './helpers/fake-auth';
import configureSSO from './helpers/sso';

const dev = {
  …  // still the same
};

const local = {
 init: () => {},
 auth: fakeAuth,
 useSSO: false,
 apiUrl: 'http://localhost:3007'
};

const prod = {
 … // still the same
};

let config;

if (process.env.REACT_APP_STAGE === 'production') {
 config = prod;
} else if (process.env.REACT_APP_STAGE === 'local') {
 config = local;
} else {
 config = dev;
}

config.init();

export default {
 TITLE: 'VU Catalog',
 ...config
};

Listing 9. An adjustment to config.js confirms the fake back end is working (src/config.js).

Introducing a local configuration lets us set the API URL to where the fake back end is running. We’ll just add one last script to package.json:

"start:local": "REACT_APP_STAGE=local react-scripts start",

Now we are set to start our base project in the local environment. Let’s start it up!

Starting the project with the fake back end

Open a terminal for the back end and run npm start. You should see the back end provide information about the collections it is serving, as shown in Figure 1.

A screenshot showing the fake back end running on localhost.

Figure 1. The fake back end starts up.

In a separate terminal, start the base project by running npm run start:local. Note that when your component loads and calls the service, you will see it hit the back end, as shown in Figure 2.

A screenshot showing a call for catalog 200.

Figure 2. The service calls a catalog from the back end.

This setup is simple, but it allows you to test your data and workflows without connecting to a real back end.

Integrating with the dev environment

Even if you are using a fake back end to test your application with various data sets, you will eventually need to connect to a real back end. I’ll conclude by showing you how to transfer from the fake back end to an actual application environment. I’ll also show you how to work around a cross-origin issue that comes up the first time you attempt to retrieve data from a real back end.

Switching to the dev config

The first thing you’ll do is to modify the apiUrl value in your application config file, shown in Listing 1. Just switch it to the dev config. Next, go to the local environment where the back end lives, and start up the front end with the npm start script. This change will start your application with the config newly pointed to your development back end.

When you first attempt to retrieve data from the back end, you will get a surprise. If you open up the web console and inspect what’s going on, you’ll see an error like this one:

Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

What to do when you’re blocked by CORS

As I mentioned at the beginning of this article, modern browsers provide safe browsing by blocking cross-origin requests, also known as CORS. If your back-end service does not explicitly authorize localhost for access, the browser will block your request. Fortunately, you can address this issue by inserting middleware into the express server on the front end. In React.js, we can introduce a file called setupProxy.js into the application’s src folder, as shown in Listing 10:

const proxy = require('http-proxy-middleware');
const BACKEND = 'http://www.example.com';

module.exports = app => {
 if (process.env.REACT_APP_STAGE === 'dev') {
   app.use(
     '/catalog',
     proxy({ target: BACKEND, changeOrigin: true, logLevel: 'debug' })
   );
 }
};

Listing 10. Add proxy middleware to the source folder (src/setupProxy.js).

Setting the logLevel to debug is optional, but it will help you see exactly what’s happening when you make a request to the back end. The key here is the changeOrigin flag. Setting this flag ensures that outbound requests will set the Origin header to point to the real back end. This way, you can avoid having your request bounced back.

Now you can test out your front-end changes with a real back end, and you can verify these changes in the cluster before creating a pull request.

Updating the production configuration

The last thing you’ll do is configure the production server. This server will use your company’s SSO or another authentication mechanism, and it will be hosted at an origin that satisfies the back end’s CORS configuration requirements. You can use the environment variable REACT_APP_STAGE=production to set this up. Note that if you used a container to host your front end, you would use the container image for this config. I’ll leave the topic of managing environmental variables for another article.

Conclusion

The next time you find yourself working on a front-end development project, consider using the three-headed configuration I’ve introduced in this article. To get started, clone or fork the GitHub project associated with the three-headed config. You can also contact Red Hat Services if you need help.

Share