Deploy a React + Express app on Hostinger
Deploying a React frontend is straightforward. Deploying a React frontend together with an Express backend — as a single cohesive app — is where most tutorials fall short. This guide covers the full picture: structuring your project, building for production, and getting both the frontend and backend running on Hostinger with the right plan for your needs.
What this guide covers
A React + Express app has two parts that need to work together in production:
Shared hosting can serve the React frontend but can't run a persistent Node.js process — for that you need Cloud Hosting or VPS. This guide covers both scenarios.
Project structure
The cleanest setup for a React + Express app is a monorepo with separate client and server folders:
myapp/
client/ ← React + Vite frontend
src/
dist/ ← built output (after npm run build)
package.json
vite.config.js
server/ ← Express backend
index.js
routes/
package.json
package.json ← root scripts for both Root package.json scripts that make your life easier:
{
"scripts": {
"build": "cd client && npm run build",
"start": "node server/index.js",
"dev": "concurrently \"cd client && npm run dev\" \"nodemon server/index.js\""
}
} Configuring Express to serve the React frontend
In production, Express should serve your React frontend's static files directly — one server, not two. Add this to your server/index.js:
import express from 'express'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
const PORT = process.env.PORT || 3001
app.use(express.json())
// Your API routes
app.use('/api', yourApiRouter)
// Serve React frontend in production
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../client/dist')))
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../client/dist/index.html'))
})
}
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
}) The app.get('*') catch-all handles React Router — any route that isn't an API call gets served index.html and React Router takes over client-side.
Configuring Vite for production
In development you likely proxy API calls to avoid CORS issues. In production, since Express serves both the frontend and the API on the same origin, no proxy is needed.
Keep the dev proxy in client/vite.config.js:
export default {
server: {
proxy: {
'/api': 'http://localhost:3001'
}
}
} In your React code, use relative URLs for API calls:
const response = await fetch('/api/users') This works in both development (proxied to Express) and production (served by Express directly).
Option A: Deploy on Hostinger Cloud Hosting or VPS
This is the recommended path. You need a Node.js runtime, which shared hosting doesn't provide. For full VPS setup (Nginx, PM2, SSL), see the dedicated VPS guide. The Express-specific Nginx block:
server {
listen 80;
server_name YOUR_DOMAIN www.YOUR_DOMAIN;
location /api/ {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
} All traffic goes through Express — both API requests and the React frontend. Express handles serving static files via express.static.
Start your app with PM2:
cd /var/www/myapp
npm install
npm run build
NODE_ENV=production pm2 start server/index.js --name myapp
pm2 save Option B: Split deploy on Shared Hosting (frontend only)
If you only have shared hosting and want to host your Express API elsewhere (a separate VPS, Railway, Render), you can still host the React frontend on Hostinger. Build your frontend and upload client/dist/ to public_html, then configure your React app to call your API at its external URL:
const API_URL = import.meta.env.VITE_API_URL || ''
const response = await fetch(`$${API_URL}/api/users`) Set VITE_API_URL during build:
VITE_API_URL=https://api.yourdomain.com npm run build Make sure your Express backend has CORS configured:
import cors from 'cors'
app.use(cors({
origin: 'https://yourdomain.com'
})) This split setup adds complexity — two separate deployments, CORS to manage, two bills. The VPS option where everything runs together is cleaner for anything beyond a prototype.
Environment variables
Never hardcode secrets. On Hostinger VPS, use a .env file:
NODE_ENV=production
PORT=3001
DATABASE_URL=your_database_url
JWT_SECRET=your_secret Load it with the dotenv package:
import 'dotenv/config' Make sure .env is in your .gitignore — never commit secrets to Git. On shared hosting, set environment variables through hPanel under Advanced → PHP Configuration or upload a .env file directly.
Deploying updates
On VPS, a straightforward update script:
cd /var/www/myapp
git pull
npm install
npm run build
pm2 restart myapp Save this as deploy.sh, make it executable (chmod +x deploy.sh), and run it with ./deploy.sh. For fully automated deploys, trigger this via a GitHub Action on push to main.
Troubleshooting
API calls return 404
Check that your Express routes are prefixed with /api/ and that your Nginx config proxies /api/ to your Node.js port. Also verify PM2 shows your app as online.
React app shows blank page
Express can't find the client/dist folder. Double-check the path in express.static() relative to where server/index.js lives.
CORS errors in the browser
You're making cross-origin API calls. Either put everything on the same origin (recommended) or configure the cors package on your Express server with the correct allowed origin.
Environment variables undefined
Make sure dotenv is imported at the very top of your entry file, before any other imports that might use process.env.
PM2 shows app as errored
Run pm2 logs myapp to see the actual error. Usually a missing dependency (npm install not run after git pull) or a wrong file path.
Summary
A React + Express app on Hostinger is best deployed as a single unit: Express serves both the API and the static React frontend, Nginx sits in front handling HTTPS, and PM2 keeps the Node.js process alive. This gives you full control, no cold starts, no function timeouts, and one clean deployment to maintain.
From $4.99/mo with full root access — perfect for Node.js + React in production.
Get a Hostinger VPS →