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.
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( <: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.
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.
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.
Last updated: September 5, 2023