Managing multiple services within a shared codebase can simplify development workflows and reduce duplication, especially when services share components and utilities. However, this approach also comes with challenges, including organizing code, optimizing builds, and supporting independent development environments. For small-scale projects, breaking services into separate repositories or packages can introduce unnecessary operational overhead. Instead, a well-structured shared codebase with thoughtful configuration ensures efficiency while leaving room for future scalability.
At RocketBox, we rely on React, Go and PostgreSQL to deliver quality services for our customers. The project is structured to keep the team from stumbling over each other when submitting PR’s and reduce conflicts in the merge process. This includes managing a marketing page, the application page, admin interfaces and more. The React applications are built with Vite which has yielded a huge performance increase over traditional React builders and bundlers. We can adjust our vite config to include import aliases, dev and build customizations and more.

The Shared Codebase Approach
With this setup, multiple React-based services share a single repository to streamline development. Each service has its own entry point, static assets, and build output, while reusable components and utilities live at the same level as the service entrypoints. This arrangement reduces duplication and avoids the overhead of managing separate repositories.
We ensure:
- Each service (e.g.,
marketing
andadmin
) has its own directory underentrypoints/
. - Reusable components and utilities are stored in the normal
components/
,utils/
,etc/
directories available for import by all services.
Building Multiple React Entrypoints
Most React applications have a single App
entrypoint and index.html
to build and bundle. We ran into issues when trying to overwrite our Vite configuration to support this approach, where our index.html
have different structures, layouts and dependencies and couldn’t be reused. Building with different indexes was not an issue, but ensuring normalization was something that proved to be a challenge. The output of the built index.html
would follow the directory structure passed to the rollup.input
configuration. This meant we had to rewrite or have an extra step to rename the file.
To remove this extra step post-build, we introduced a new output plugin to perform the rewriting for us:
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig(({ mode }) => {
const entryDir = mode ? `src/entrypoints/${mode}` : 'src';
const publicDir = mode ? `src/entrypoints/${mode}/public` : 'assets';
const outDir = mode ? `../cmd/${mode}/www` : './dist';
return {
root: path.resolve(__dirname, entryDir),
publicDir: path.resolve(__dirname, publicDir),
plugins: [react()],
resolve: {
alias: {
'@rocketbox-react': path.resolve(__dirname, './src'), // Maps to current shared directory for future package migration
},
},
server: {
host: '127.0.0.1',
port: mode === 'marketing' ? 3001 : 3000,
open: true,
},
build: {
outDir,
rollupOptions: {
input: {
main: path.resolve(__dirname, `${entryDir}/index.html`),
},
output: {
entryFileNames: 'assets/[name].js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name].[ext]',
plugins: [
{
name: 'html-path-rewrite',
generateBundle(_, bundle) {
for (const key in bundle) {
if (bundle[key].type === 'asset' && bundle[key].fileName.endsWith('.html')) {
// Rewrite the path for the HTML file
bundle[key].fileName = 'index.html';
}
}
},
},
],
},
},
},
};
});
This approach solves our 3 primary concerns:
- The
html-path-rewrite
plugin ensures that theindex.html
file is output directly at the root of theoutDir
, rather than preserving unnecessary directory structure. This is particularly important for multi-service builds where each service has its own entry point. - The paths for
root
,publicDir
, andoutDir
are dynamically determined based on the service being built or served. This keeps the configuration DRY while supporting multiple services. - The
input
ensures the correctindex.html
is used for each service, while theoutput
plugins and file names create clean, predictable build artifacts.
Building and Running the Dev Server
With the updated configuration, scripts defined in package.json
make it easy to build and serve services independently:
"scripts": {
"dev": "vite",
"dev:marketing": "vite --mode marketing",
"dev:admin": "vite --mode admin",
"build:marketing": "vite build --mode marketing",
"build:admin": "vite build --mode admin"
}
The yarn dev:marketing
command starts a development server for the marketing
service on port 3001
, ensuring the html-path-rewrite
plugin applies to the build paths during local development.
Lastly, the yarn build:admin
command creates a production-ready build for the admin
service, outputting a clean directory structure with the index.html
placed directly in cmd/admin/www/
.
Conclusion
Configuring Vite to build multiple pages is a great way to simplify the code base when projects are in their infancy. The best part is we have flexibility with Vite import aliases to move @rocketbox-react
to it’s own package, without needing to update our entire codebase. We also have the flexibility to break out our entrypoint with a small adjustment to the entrypoint file, apply standard Vite configurations and build within it’s own repository as the codebase grows.
Member discussion