Optimizing Deployment: How to Deploy Multiple Applications with a Single Codebase
In a typical React project, the most common folder structure looks like this:
/my-react-app
├── /public
│ ├── index.html
│ ├── manifest.json
├── /src
│ ├── /assets
│ │ ├── /images
│ ├── /components
│ │ ├── Blabla.js
│ ├── App.js
│ ├── index.js
│ └── App.css
├── .gitignore
├── package.json
├── package-lock.json
└── README.md
This structure is suitable for deploying a single application, where the source code is designed for one product. However, what happens when we want to deploy multiple applications from the same codebase? Let’s imagine we want to create two (or more) applications that function as independent products, each with its own domains, URLs, <head>
tags, and manifest.json files, but still using the same codebase.
This was exactly my case.
Case Study: Arithmental and Speed Calculation
A year ago, I began working on Arithmental, a web application focused on mental calculation training through games and personalized classes. Initially, we followed this traditional folder structure.
Later, we decided to launch a new product, Speed Calculation, a free platform with constant challenges to test your mental calculation skills. Although both products have different business models, they share the same codebase for the games, visual components, and other elements. However, we needed to deploy both applications separately, generating different navigation, even for components that were identical.
This scenario presented a challenge in the traditional React structure, which is primarily designed for a single application. We considered the following solution:
- Duplicating the Project into a New Repository: One option would be to copy and paste the necessary files into a new repository, allowing us to have custom routes, index.html, and manifest.json files for each application. However, this would increase the code maintenance, as any changes in the base code would need to be manually replicated in both projects, which is unsustainable for a small team.
Understanding React’s Build Process
To understand the solution, it’s essential to explore the build process of a React application and the behavior of the react-scripts build
(or npm run build
) command. When executing this command, React performs a series of tasks that minify all the JavaScript code to optimize the application’s loading speed. Additionally, the code is split into “chunks” to load only the necessary parts at the right time.
The index.html file is the entry point React uses to display the application in the browser. During this process, resources are injected into index.html, including metadata and references to generated CSS and JavaScript files like main.js
and main.css
, ensuring that the browser loads the styles and scripts correctly.
With this understanding of the build process, it becomes clear that routing plays a crucial role in configuring our applications. Since we need two applications to operate independently, it’s essential that the routing reflects this separation.
Routing
We implemented two different route files, ensuring that for the same component, different routes were used. Here’s an example:
export const SpeedCalculationRoutes = [
<Route path="/speedcalculation/static/simpleAdittion" element={<SimpleStatic />} />,
]
export const ArithmentalRoutes = [
<Route path="/static/simpleAdittion" element={<SimpleStatic />} />,
]
In the App.js
file, we implemented a conditional logic that allows rendering different routes depending on the hostname:
function App() {
const hostname = window.location.hostname;
let routes;
if (hostname === 'arithmental.com') {
routes = ArithmentalRoutes;
} else if (hostname === 'speedcalculation.com') {
routes = SpeedCalculationRoutes;
} else {
routes = [...ArithmentalRoutes, ...SpeedCalculationRoutes];
}
return (
<Routes>
{routes}
</Routes>
);
}
Then, we configured our deployment using Cloudflare Pages, ran npm run build
, and voilà! Depending on the host, we can show separate behavior. But wait! For the search engine, it’s like we’re on the same application since the index.html — that is, the application’s metadata — is the same.
As we saw in the build process, all the code is bundled into the index.html, and so far, we’ve only changed the routing.
Multiple index.html Files in the Build
We now face the need to select which metadata to use depending on the host. However, using conditionals won’t be the solution, as the index.html becomes static at build time; it doesn’t form part of the dynamic selection process.
Get ready, because the following will surprise you: humanity’s greatest engineering solution… Just kidding! The solution is quite simple (though it was a real headache not finding any references about it; hence, I’m building this source of information now). We know the index.html won’t move during the build, and we need it to have different metadata for each deployment. The answer is to have different index.html files.
I know it sounds basic, but hang on a moment. How do we choose which one to use? Here’s where it gets interesting: since we can execute a command in the console before the build process, we could generate two different commands for each build, specifying which files to use.
This leads us to a new folder structure, which only changes the public folder:
/my-react-app
├── /public
│ ├── index.html
│ ├── manifest.json
│ ├── /speedcalculation
│ │ ├── index.html
│ │ ├── manifest.json
│ ├── /arithmental
│ │ ├── index.html
│ │ ├── manifest.json
├── /src
│ ...
└── README.md
As you can see, we’ve added two folders for each application, each containing its own index.html and manifest.json files. The files in the root are those consumed by the application as part of a routine process. To facilitate this in the package.json (where script commands are defined), we added a bash command before performing the build:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build:speedcalculation": "rm -rf build && cp public/speedcalculation/index.html public/index.html && cp public/speedcalculation/manifest.json public/manifest.json && react-scripts build",
"build:arithmental": "rm -rf build && cp public/arithmental/index.html public/index.html && cp public/arithmental/manifest.json public/manifest.json && react-scripts build",
},
What happens in deployment is that the command to execute is configured as npm run build:application
, and the corresponding files will be copied from each folder to where React expects them when executing react-scripts build
. This will transpile the JavaScript and CSS files into the file containing the metadata of the application that will run.
This way, we solved the issue of having the deployed application use its own metadata. When the corresponding build is made, the necessary files are copied to the correct location. Thus, we managed to use the same codebase to deploy two applications that, from the user’s and browser’s perspective, are completely different.
Our journey from a traditional React application to a solution that allows deploying multiple applications from a common codebase illustrates how innovation arises from necessity. Let’s recap the key points of our solution:
- Dynamic Routing: We implemented routing based on the hostname to differentiate between applications.
- Modified Folder Structure: We adopted a structure that houses multiple index.html and manifest.json files.
- Custom Build Scripts: We created specific scripts for building each application.
This approach not only solves the immediate problem of deploying Arithmental and Speed Calculation as independent products, but it also establishes a flexible framework for future projects that can benefit from a shared codebase.
It’s important to remember that, while this solution worked exceptionally well for our specific case, each project has its own particularities and unique challenges.
Have you faced similar difficulties in your projects? Do you have ideas on how we could further improve this solution? I’d love to hear your thoughts and experiences.
Let’s stay connected! If you enjoyed this article, feel free to leave a comment, share, and applaud — this helps the community thrive. You can also follow me on LinkedIn to stay updated on my upcoming posts and share knowledge along the way.