DEV Community

Cover image for Building a Multi-Tenant React App. Part 1: Setup
José Del Valle
José Del Valle

Posted on • Originally published at delvalle.dev on

Building a Multi-Tenant React App. Part 1: Setup

Before getting into today's post, I am offering free sessions to React beginners. Wanna participate? You just have to follow me on Twitter and DM me telling your experience, what do you like about React or Web Dev in general and your goals as a Web Dev. More info here.


Real-world scenario (cause it happened to me).

You work for a company that offers websites to different clients so they can showcase their products. All these websites share the same layout, the same core features and most of the components. There is some degree of flexibility allowed. For example, clients can choose the color theme and which routes to enable or disable. They can also choose which page or component to show on each route. Of course each client will have different content on their website.

The decision made is to only build one web application, flexible enough to handle these use cases instead of working on custom websites for each client. Clients are aware they'll share the layout and most of the styles.

In the end, we had about eight different clients. Two of them had two different sites. Each with a different color theme. We also ended up offering two different layouts and in some cases different components -some clients wanted very specific functionalities-.

How did we handle that? We received the client-specific configuration from the API and rendered everything dynamically client-side. Today I'll cover the project setup so we start with a good base. In future posts, I'll explain how we got to manage custom themes, custom routes, and custom components as per the clients' requests.

Initializing the project

Let's first create our folder structure and our client-side app. I created a folder called multi-tenant-react-app. Let's open the terminal, go to this folder and do npm init. It will ask you to type some info. Here's what I entered -the git repo is the one I'm gonna use for this series so you can leave this blank or add a repo of yours, I also called the entry point server.js instead of index.js-.

package name: (multi-tenant-react-app) 
version: (1.0.0) 
description: A multi tenant React app with server
entry point: (index.js) server.js
test command: 
git repository: (https://github.com/dlvx/multi-tenant-react-app.git) 
keywords: 
author: José Del Valle
license: (ISC) 

Inside this same folder, we have to create our server.js file and initialize our react app with create-react-app. I'm gonna call it client because we are also going to work on a simple server using Node.

npx create-react-app client 

We should end up with the following structure:

- multi-tenant-react-app
  - /client
  - server.js

Remember server.js is outside of the client folder.

Creating a basic server

Our server will serve the different config files to our client app so it knows what to render. For now, we'll install express to help us serve these config files. We are also gonna install the cors and nodemon packages. Make sure you're still in your root directory and do:

npm install express cors

You can choose to install nodemon globally or as a dev dependency. I have it globally.

npm install -g nodemon

Lets now add the script to run our server in the package.json file. Add the following line inside the scripts section:

"server": "nodemon server.js",

Ok, let's now code our server. We first have to set it up so it listens on a specific port and also accepts requests from another origin as we'll run our React development server on a different port.

const express = require('express');
const cors = require('cors');

// Setup
const PORT = process.env.SERVER_PORT || 4000;
const app = express();
app.use(cors());

// Routes
app.get('/getConfig', async function (req, res) {
  const { clientId } = req.query;
  /**
   * 
   * We'll add more code here later
   * 
  **/
});

// Run server
app.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});

We've added an endpoint called getConfig which will receive the client id from our react app and will return the config file specific to that client. For now, we're gonna handle the client id in our React app using environment variables but I'll return to that in a bit.

In the real world, we were storing the client configurations in a database but for this tutorial, I'm gonna stick with JSON just so you get the idea.

Let's create our JSON database now. Add a new folder called db , at the root level. Inside it lets add a file called configs.json. We should end up with the following structure:

- multi-tenant-react-app
  - /client
  - /db
    - configs.json
  - server.js

We'll now add some configuration for our clients in the configs.json file. Something simple for now so we can test this soon:

[
  {
    "clientId": 1,
    "name": "Client A"
  },
  {
    "clientId": 1,
    "name": "Client B"
  }
]

Serving client-specific configuration

Now, to serve each client configuration we need a way to get it from our JSON database and return it to the client-side app.

Let's add a new folder at the root level called model and create a new file inside it called config.model.js. For now, we'll add a simple function that will find the client config given an id:

const configs = require('../db/configs.json');

function getClientConfig(clientId) {
  return configs.find(config => config.clientId == clientId);
}

module.exports = {
  getClientConfig
}

Now, in our server.js we'll import this model so we can use it in our getConfig endpoint.

const Config = require('./model/config.model');

The getConfig endpoint will now look something like this:

app.get('/getConfig', async function (req, res) {
  // Receive the clientId from our client-side app
  const { clientId } = req.query;

  // Find the config for that particular clientId
  const clientConfig = Config.getClientConfig(clientId);

  if(!clientConfig){
    // Return an error if it's not found
    res.status(404).send({ error: `Config not found for this clientId: ${clientId}` });
  }

  // Send the config if found
  res.send(clientConfig);
});

We are now ready to communicate with the client-side app and send the config it needs.

Lets finally move to our React app.

Receiving the configuration in our React app

Go back to the terminal again and move to the client folder. Lets first run our app to make sure everything was set up correctly:

cd client && npm start

You should be able to see create-react-app's default home screen.

If everything's ok, let's install axios which will help us make requests to our server:

npm install axios

We need to create a new folder and some files. Create a folder called services and two files inside: axiosSetup.js and config.service.js.

Here's what we need to put in axiosSetup.js :

import axios from 'axios';

const instance = axios.create({
  baseURL: `http://localhost:4000`,
});

export default instance;

Basically, we're just creating an instance that will communicate with our server running on port 4000. We export this instance so we can re-use it in our services.

As for config.service.js we need to add the function that will make the GET request through the axios instance:

import axiosInstance from './axiosSetup';

async function getConfig(){
  try {
    return await axiosInstance.get('/getConfig', {
      params: {
        clientId: process.env.REACT_APP_CLIENT_ID
      }
    });
  } catch(e){
    return e.response;
  }
}

export {
  getConfig
}

We export the getConfig function so we can use it in App.js. As you can see, I'm sending the clientId to the server so it can find the correct config in the DB.

In production we didn't use environment variables to set the client id in the client-side but for the sake of the tutorial, it is just simpler this way.

One last step now. Lets go to App.js and import the getConfig service:

import { getConfig } from './services/config.service';

Besides that, we need to import the useEffect and useState hooks, so your first line would be something like this:

import React, { useState, useEffect } from 'react';

Inside our App component, we'll make use of useEffect to call getConfig when the component initially mounts and we'll use useState to manage our response status and store the config when available.

As for the JSX, we'll get rid of some things and add some status messages. It will show the name we set in each client config in our DB if the response is successful.

Our component will end up being like this:

import React, { useState, useEffect } from 'react';
import logo from './logo.svg';
import './App.css';
import { getConfig } from './services/config.service';

function App() {

  const [config, setConfig] = useState({ loading: true, data: {} });
  const { loading, data } = config;

  useEffect(() => {
    async function getConfigAsync(){
      const { data } = await getConfig();
      setConfig({ data });
    }

    getConfigAsync();
  }
  , []);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          {
            loading && 'Getting config from server...'
          }
          {
            data.error && 'Error getting config from server'
          }
          {
            data.name && `The client is: ${data.name}`
          }
        </p>
      </header>
    </div>
  );
}

export default App;

Let's try this out!

We can finally run our app! Make sure you're still in the client folder.

We'll first test our error scenario. Let's just run the following command without setting a client id:

npm start

If everything was set correctly and the React app can communicate properly with the server you'll see the following message:

Error getting config from server.

It failed cause we didn't set a client id in the environment variables so the server didn't find the config for this app.

Now, stop the app and add the client id to the environment variables and run the app again with the following command. Like so:

REACT_APP_CLIENT_ID=1 npm start

What do you get? I hope you see the message: The client is: Client A

Now, what if you set the client id to 2 and run the following?

REACT_APP_CLIENT_ID=2 npm start

You should see: The client is: Client B

You can run both client apps simultaneously if you set a different port for one of them, for example:

REACT_APP_CLIENT_ID=1 npm start

and

PORT=3002 REACT_APP_CLIENT_ID=2 npm start

Both will communicate with the same server and will receive their specific config.


Ok! We're finally done for the moment. This is just the tip of the iceberg. We've only shown the client's name! In the next posts, we'll see how we can have different routes per client and then different component configurations.

Here's the Github repo in case you want the whole project.

Stay tuned and thanks for reading!

Follow me on twitter: @jdelvx

Discussion (0)