Recommended snack and song:
Have a lovely bowl of rice pudding while listening to Auto!Automatic!!
Loopback is amazing at REST API Service, and create-react-app is a neat little CLI that generates a solid starting point for a SPA (single page application). But putting them together can be… tricky!
Nevertheless, we’ll give it a shot.
tl;dr: Go clone the repo and change to full-example, it has all of the code in there.
What you’ll need to follow along
Let’s clone the repo and install dependencies, plus the create-react-app package:
1 2 3 4 5 | $ npm install -g create-react-app $ git clone https://github.com/danazkari/take-note.git $ cd take-note $ npm install $ npm start |
The server should now be running at http://localhost:3000 and should have a couple of notes already in there for us to play with. (You can go ahead now and stop the server with Ctrl + C).
SPA supporting mods to our Loopback instance
First, go ahead and open the server/config.json file, and add indexFile key to the root of the config like so:
1 2 3 4 | { "indexFile": "client/build/index.html", ... } |
When you run the production build, that’s where our front-end app will live as a production-ready app. We want a reference to that. Trust me.
Next up, let’s edit the server/server.js
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 'use strict'; var loopback = require('loopback'); var boot = require('loopback-boot'); // 1. Include 'path' package var path = require('path'); var app = module.exports = loopback(); app.start = function() { // 2. Get the FQPN of the index file in client var staticFolder = path.dirname( path.resolve(__dirname, '..', app.get('indexFile')) ); // 3. Set staticFolder as static in the server app.use(loopback.static(staticFolder)); // start the web server return app.listen(function() { app.emit('started'); var baseUrl = app.get('url').replace(/\/$/, ''); console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { var explorerPath = app.get('loopback-component-explorer').mountPath; console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } }); }; // Bootstrap the application, configure models, datasources and middleware. // Sub-apps like REST API are mounted via boot scripts. boot(app, __dirname, function(err) { if (err) throw err; // start the server if `$ node server.js` if (require.main === module) app.start(); }); |
So what we’ve done is included the path package and used it to serve our indexFile config path as a static folder. Pretty simple, right? Next.
For our next modification, we need to tell our client that every request that comes along through /, should resolve to indexFile. We do that through editing server/boot/root.js like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 'use strict'; // 1. Import the 'path' package var path = require('path'); module.exports = function(server) { // 2. Move server status to '/status' // Install a `/status` route that returns server status var router = server.loopback.Router(); router.get('/status', server.loopback.status()); // 3. Configure '/' to serve the static content router.get('/', function(req, res) { var indexFile = path.resolve(__dirname, '../..', server.get('indexFile')); res.sendFile(indexFile); }); server.use(router); }; |
In order for us to run both the back-end and front-end in parallel, we need to install a package called concurrently, which will make our lives a lot easier.
1 | $ npm install --save concurrently |
And our last bit of work on the back-end side is to add some scripts to our package.json file like so:
1 2 3 4 5 6 7 8 9 10 11 | { ... "scripts": { "lint": "eslint .", "prestart": "npm run --prefix client build", "start": "node .", "start-dev": "concurrently \"nodemon .\" \"npm start --prefix client\"", "postinstall": "npm install --prefix client", "posttest": "npm run lint && nsp check" } } |
First up, we added a prestart step because, before our app runs, we want to make sure we have a client folder to serve as static, so we run a build command on behalf of our react app (I know… we haven’t set up the front-end yet, bear with me); then, we have the start-dev script which runs both our back-end and front-end dev servers at the same time; there’s also the postinstall step, so we also run the install script on behalf of our front-end app when installing our dependencies.
And that’s it for our back-end mods! Easy, right?
Let’s create and configure our React app!
Remove the client/README.md file, and call the create-react-app generator on the client directory:
1 2 | $ rm client/README.md $ create-react-app client/ |
Before running our newly created front-end app, we need to make a minor change and configure it to run in port 3001 instead of 3000, because it would collide with our back-end app.
To do so, we need to add a client/.env file:
1 | PORT=3001 |
One last thing: I encountered a problem with the service worker, where it would take over the routing completely (once the app is served for production/staging environment) and not allow you to query the back-end server. I solved this problem temporarily by commenting out the lines where it’s imported and then registering it in client/src/index.js:
1 2 3 4 5 6 7 8 | import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; // import registerServiceWorker from './registerServiceWorker'; ReactDOM.render(, document.getElementById('root')); // registerServiceWorker(); |
Not entirely happy with this, but for now, it gets the job done.
And that should be it!
Playtime!
Just to make sure everything is working, execute:
1 | $ npm install |
This should install our dependencies and then try to install the front-end dependencies as well.
Now let’s run the dev servers:
1 | $ npm run start-dev |
This should launch our back-end server at http://localhost:3000 and should have opened up your browser at http://localhost:3001.
Let’s test if everything connects correctly by modifying the client/src/App.js like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; class App extends Component { constructor(props) { super(props); this.state = { notes: [], }; } componentWillMount() { fetch('http://localhost:3000/api/notes') .then(response => response.text()) .then(JSON.parse) .then(notes => this.setState({ notes })); } render() { const { notes } = this.state; return ( |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; class App extends Component { constructor(props) { super(props); this.state = { notes: [], }; } componentWillMount() { fetch('http://localhost:3000/api/notes') .then(response => response.text()) .then(JSON.parse) .then(notes => this.setState({ notes })); } render() { const { notes } = this.state; return ( <div> <header> <img src="{logo}" alt="logo" /> <h1>Welcome to React</h1> </header> <p> To get started, edit <code>src/App.js</code> and save to reload. </p> <ul> {notes.map(({ id, title, text }) => <li><b>{title}</b> - {text}</li>)} </ul> </div> ); } } export default App; |
Yes, I agree: the UI looks terrible like that. But don’t concentrate on that; look at what we’ve accomplished! We have a fully capable React application running inside an instance of another fully capable REST API service.
Pat yourself on the back, go get yourself another bowl of rice pudding and keep learning! You deserve it!
I think it is important to understand what the service worker is supposed to do. https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#making-a-progressive-web-app
Not sure what is the tricky part, looks like any node/express/any other node framework setup for a SPA.
Also, I think you are losing a lot of the potential from create react app by just building it and serving is statically. Where are the benefits of hot reloading and stuff like that?
I’d prefer to see a good setup to run/debug/hot reload both apps Front End and Back End with just one command.
Hello Danny!
To answer your concerns individually:
1. While service workers are awesome indeed, this one was messing with the routing and intercepting the communications with the REST server. I would have loved to fix the service worker so it ignored the paths that were REST specific (and while I’m sure there are solutions for this), I didn’t want to focus too much on fixing the service worker to ignore the REST paths and focus more on the REST server communication.
2/3/4. While on development mode (that is launching
npm run start-dev
), you get hot reloading for your React app and auto-reloading for your backend server (through thenodemon
package).If you wanna see it in action, I invite you to:
$ git clone https://github.com/danazkari/take-note
$ cd take-note
$ git checkout full-example
$ npm install
$ npm run start-dev
I promise you that if you run those commands in that order, you’ll end up with a one command with all you described, hot/auto reloading upon file changes on both backend and frontend!
Happy coding and thanks for raising your concerns!
Thanks Danny for this timely tutorial.
What does a production setup look like? Would loopback automatically bundle React or you have a command for that? Any extra steps before deploying to say Heroku?
Hello Josh! Glad this tutorial ended up being timely for you!
So the good thing about this setup (final version is https://github.com/danazkari/take-note/tree/full-example), when the server starts with the usual npm start, it’ll make a “production ready” build for the react on the prestart command, so by the time the server starts, the production build of the React app will be ready to rock and roll!
You can look at the pre-start command at https://github.com/danazkari/take-note/blob/full-example/package.json#L10.
For adding Heroku, I think the only steps to follow would be the usual ones at the official docs about nodejs: https://devcenter.heroku.com/articles/deploying-nodejs
After that, it should be smooth sailing! Let me know if you hit any bumps.
This is great. Thank you very much for this contribution. but I have a problem. I can not access the localhost:3000/explorer or any other path. always redirect me to indexFile. Can you helpme?
If you can’t access to localhost:3000/explorer and not allow you to query backend server, try this in your client/src/index.js
import React from ‘react’;
import ReactDOM from ‘react-dom’;
import ‘./index.css’;
import App from ‘./App’;
//import registerServiceWorker from ‘./registerServiceWorker’;
import {unregister} from ‘./registerServiceWorker’;
ReactDOM.render(, document.getElementById(‘root’));
//registerServiceWorker();
unregister();
I am afraid I am having the same issue.