Search code examples
node.jsreactjsherokuaxiosjson-server

Best practices to deploy a React app and an API on Heroku


I'm currently working on a project using React (via create-react-app) and JSONServer for the API.

I have a single Git repository structured as follow :

|-- client
|-- api

To work in a dev environment, I start both of the folders with npm start in order to have http://localhost:3000 for the React app and http://localhost:3001 for the API.

This way (thanks to Axios) I can connect to http://localhost:3001/api to retrieve my content

import axios from 'axios'

export default axios.create({
  baseURL: 'http://localhost:3001/api'
})

Everything works great so far.

Now I want to deploy my app and API to Heroku and this is where things get complicated.

So here's my questions and it would be very helpful to have some advice on what is the best way to handle it:

  • Should I create two apps on Heroku ? One for the client and one for the API?
  • If I have two apps on Heroku, how can I reach the API safely from the client? Maybe fetching the url of the API app (like https://myapp.herokuapp.com/api) but I'll have to deal with CORS issues.
  • Or should I create only one app with API and put the client's build in the same folder/url (Eg. : serving public files of the build with JSONServer)

For the moment, I have a server.js file at the root of my client's folder and a Procfile with web: node server.js to launch a server for the build. Then I fetch data with Axios from another Heroku app (which is actually the API app).

Any advice/help will be very appreciated! Thanks!

P.S.: here's my package.json ans server.js files for client and api

Client's server.js (at the root of client's folder)

const express = require('express')
const http = require('http')
const path = require('path')

const app = express()
app.use(express.static(path.join(__dirname, 'build')))

const port = process.env.PORT || '8080'
app.set('port', port)

const server = http.createServer(app)
server.listen(port, () => console.log(`Running on localhost:${port}`))

Api's server.js (at the root of api's folder)

const express = require('express');
const jsonServer = require('json-server');
const router = express.Router();
const server = jsonServer.create();
const mainRoute = jsonServer.router('db.json');
const middlewares = jsonServer.defaults({ noCors: false });
const port = process.env.PORT || 3001;

router.use('/api', mainRoute)
server.use(middlewares);
server.use(router);

server.listen(port);

Client's package.json:

{
  "name": "portfolio",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/core": "^10.0.28",
    "@emotion/styled": "^10.0.27",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "axios": "^0.19.2",
    "express": "^4.17.1",
    "google-spreadsheet": "^3.0.11",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-intl": "^4.6.3",
    "react-lazyload": "^2.6.8",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "postinstall": "react-scripts build"
  },
  "proxy": "https://my-api.herokuapp.com/api",
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "eslint-plugin-react": "^7.20.0",
    "react-spring": "^8.0.27",
    "standard": "^14.3.4"
  }
}

Api's package.json:

{
  "name": "json-server-heroku",
  "version": "1.0.0",
  "description": "Simple json-base database to deploy to Heroku",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "fetch": "node fetch-data.js"
  },
  "keywords": [
    "json-server,heroku, node, REST API"
  ],
  "author": "Jesper Orb",
  "license": "ISC",
  "dependencies": {
    "google-spreadsheet": "^3.0.11",
    "json-server": "^0.16.1"
  }
}

Solution

  • I see you are using json server, so you don't need to use express to serve your static assets, instead you need to move your build folder to root level of the project and rename it as public so that json-server will see it and serve it according to their markdown on github

    1. Move your api/package.json and server.js to the root level of your application.
    2. In order to move your client/build folder(after running npm run build from ./client) and rename it as public, add a heroku-postbuild script to the root package.json like this "heroku-postbuild": "cd client && npm install && npm install --only=dev --no-shrinkwrap && npm run build && mv -v build/ .. && cd .. && mv build public"
    3. add your main api url's to your react app as

    client/.env.development

    REACT_APP_BASE_URL=http://localhost:4000/api
    

    client/.env.production

    REACT_APP_BASE_URL=https://myapp.herokuapp.com/api
    
    1. and let the json-server serve your static frontend files automatically.