Building a full-stack app in Replit means running a React frontend and an Express backend together, with the .replit file configured to start both processes. This tutorial walks you through setting up the project structure, creating API routes in Express, adding React Router for client-side navigation, connecting to Replit's built-in PostgreSQL database, and configuring ports so everything works both in development and after deployment.
Build a Full-Stack React + Express App in Replit with Routing and PostgreSQL
Replit's default and best-supported stack is React + Tailwind CSS + ShadCN UI, backed by an Express server and PostgreSQL database. This tutorial shows you how to set up this full-stack architecture from scratch, configure the .replit file to run frontend and backend simultaneously, implement proper routing on both sides, and connect to the database. By the end, you will have a working app with a React frontend that talks to an Express API, all running in one Replit workspace.
Prerequisites
- A Replit account (Core or Pro plan recommended for PostgreSQL)
- Basic familiarity with JavaScript, React components, and Express routes
- Understanding of HTTP methods (GET, POST) and JSON
- PostgreSQL database enabled in your Replit App (Cloud tab -> Database)
Step-by-step guide
Create a new Replit App with the right template
Create a new Replit App with the right template
Click 'Create App' on the Replit dashboard. In the Agent prompt, describe your app or select 'Web App' as the project type. Agent will scaffold a React + Vite frontend with Tailwind CSS. If you prefer to set up manually, create a new Repl with the Node.js template and install dependencies via Shell: npm create vite@latest client -- --template react-ts, then npm init -y in a server directory. The Agent-generated structure is recommended because it pre-configures the .replit file and port bindings correctly.
Expected result: You have a Replit App with a client directory containing React code and a server directory ready for Express.
Set up the Express backend with API routes
Set up the Express backend with API routes
Create a server directory with an index.js file. Install Express and the PostgreSQL client: run npm install express pg in Shell. Set up a basic Express server that listens on port 3001 (keep 5173 for Vite). Create a /api/health route to verify the server is running, and a /api/users route as a starting point for your API. Use process.env.DATABASE_URL to connect to PostgreSQL, which Replit sets automatically when you enable the database.
1// server/index.js2import express from 'express';3import pg from 'pg';45const app = express();6const PORT = 3001;78app.use(express.json());910// Database connection11const pool = new pg.Pool({12 connectionString: process.env.DATABASE_URL13});1415// Health check16app.get('/api/health', (req, res) => {17 res.json({ status: 'ok', timestamp: new Date().toISOString() });18});1920// Users API21app.get('/api/users', async (req, res) => {22 try {23 const result = await pool.query('SELECT * FROM users ORDER BY id');24 res.json(result.rows);25 } catch (err) {26 console.error('Database error:', err.message);27 res.status(500).json({ error: 'Database query failed' });28 }29});3031app.post('/api/users', async (req, res) => {32 const { name, email } = req.body;33 try {34 const result = await pool.query(35 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',36 [name, email]37 );38 res.status(201).json(result.rows[0]);39 } catch (err) {40 console.error('Insert error:', err.message);41 res.status(500).json({ error: 'Failed to create user' });42 }43});4445app.listen(PORT, '0.0.0.0', () => {46 console.log(`Server running on port ${PORT}`);47});Expected result: Running node server/index.js in Shell starts the Express server on port 3001 and responds to /api/health with a JSON status.
Configure the .replit file to run both processes
Configure the .replit file to run both processes
Open the .replit file (enable Show hidden files in the file tree menu). Configure the run command to start both the Vite dev server and the Express backend simultaneously using the & operator and wait. Set the port configuration to map the frontend port (5173) to external port 80. The backend runs on 3001 internally and the frontend proxies API requests to it. This multi-process configuration is critical — without it, you can only run one process at a time.
1# .replit2entrypoint = "client/src/App.tsx"3modules = ["nodejs-20:v8-20230920-bd784b9"]45# Run both frontend and backend6run = "cd server && node index.js & cd client && npm run dev & wait"78[nix]9channel = "stable-24_05"1011[deployment]12run = ["sh", "-c", "cd server && node index.js"]13build = ["sh", "-c", "cd client && npm install && npm run build"]14deploymentTarget = "cloudrun"1516[[ports]]17localPort = 517318externalPort = 801920[[ports]]21localPort = 300122externalPort = 30012324hidden = [".config", "package-lock.json", "node_modules"]Expected result: Pressing Run starts both the React dev server and the Express API server. The Preview pane shows your React app.
Add React Router for client-side navigation
Add React Router for client-side navigation
Install React Router in your client directory: cd client && npm install react-router-dom. Set up a BrowserRouter in your App.tsx with Route components for each page. Create separate page components (Home, Users, About) in a pages directory. React Router handles navigation without full page reloads, keeping the experience fast. Wrap your entire app in BrowserRouter at the top level.
1// client/src/App.tsx2import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';3import Home from './pages/Home';4import Users from './pages/Users';5import About from './pages/About';67function App() {8 return (9 <BrowserRouter>10 <nav className="p-4 bg-gray-100 flex gap-4">11 <Link to="/" className="text-blue-600 hover:underline">Home</Link>12 <Link to="/users" className="text-blue-600 hover:underline">Users</Link>13 <Link to="/about" className="text-blue-600 hover:underline">About</Link>14 </nav>15 <main className="p-4">16 <Routes>17 <Route path="/" element={<Home />} />18 <Route path="/users" element={<Users />} />19 <Route path="/about" element={<About />} />20 </Routes>21 </main>22 </BrowserRouter>23 );24}2526export default App;Expected result: Clicking navigation links switches between pages without a full reload. The URL bar updates to reflect the current route.
Connect the React frontend to the Express API
Connect the React frontend to the Express API
Configure Vite to proxy API requests to your Express backend during development. Open vite.config.ts in the client directory and add a proxy rule. This forwards any request starting with /api from the Vite dev server (port 5173) to Express (port 3001). In production, both frontend and API run on the same Express server, so no proxy is needed. Then create a React component that fetches data from your API and displays it.
1// client/vite.config.ts2import { defineConfig } from 'vite';3import react from '@vitejs/plugin-react';45export default defineConfig({6 plugins: [react()],7 server: {8 host: '0.0.0.0',9 port: 5173,10 proxy: {11 '/api': {12 target: 'http://localhost:3001',13 changeOrigin: true14 }15 }16 }17});1819// client/src/pages/Users.tsx20import { useEffect, useState } from 'react';2122interface User {23 id: number;24 name: string;25 email: string;26}2728export default function Users() {29 const [users, setUsers] = useState<User[]>([]);3031 useEffect(() => {32 fetch('/api/users')33 .then(res => res.json())34 .then(setUsers)35 .catch(console.error);36 }, []);3738 return (39 <div>40 <h1 className="text-2xl font-bold mb-4">Users</h1>41 <ul className="space-y-2">42 {users.map(user => (43 <li key={user.id} className="p-2 bg-gray-50 rounded">44 {user.name} — {user.email}45 </li>46 ))}47 </ul>48 </div>49 );50}Expected result: The Users page fetches data from /api/users and displays it. The Vite proxy transparently forwards the request to Express.
Serve the React build from Express in production
Serve the React build from Express in production
For deployment, Express needs to serve the compiled React files and handle client-side routing. Add express.static middleware pointing to the client build output directory. Add a catch-all route after all API routes that serves index.html for any non-API request. This ensures React Router handles the routing in production. Update your deployment build command in .replit to compile the frontend before starting the server.
1// Add to server/index.js — after all API routes2import path from 'path';3import { fileURLToPath } from 'url';45const __dirname = path.dirname(fileURLToPath(import.meta.url));67// Serve React build files in production8if (process.env.REPLIT_DEPLOYMENT) {9 app.use(express.static(path.join(__dirname, '../client/dist')));1011 // Catch-all: send index.html for client-side routes12 app.get('*', (req, res) => {13 res.sendFile(path.join(__dirname, '../client/dist/index.html'));14 });15}Expected result: After deployment, visiting any URL on your app loads the React frontend, and /api routes return JSON data from Express.
Complete working example
1// server/index.js — Full-stack Express server2import express from 'express';3import pg from 'pg';4import path from 'path';5import { fileURLToPath } from 'url';67const __dirname = path.dirname(fileURLToPath(import.meta.url));8const app = express();9const PORT = process.env.PORT || 3001;1011app.use(express.json());1213// Database connection14const pool = new pg.Pool({15 connectionString: process.env.DATABASE_URL16});1718// API Routes19app.get('/api/health', (req, res) => {20 res.json({ status: 'ok', timestamp: new Date().toISOString() });21});2223app.get('/api/users', async (req, res) => {24 try {25 const result = await pool.query('SELECT * FROM users ORDER BY id');26 res.json(result.rows);27 } catch (err) {28 console.error('Database error:', err.message);29 res.status(500).json({ error: 'Database query failed' });30 }31});3233app.post('/api/users', async (req, res) => {34 const { name, email } = req.body;35 if (!name || !email) {36 return res.status(400).json({ error: 'Name and email required' });37 }38 try {39 const result = await pool.query(40 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',41 [name, email]42 );43 res.status(201).json(result.rows[0]);44 } catch (err) {45 console.error('Insert error:', err.message);46 res.status(500).json({ error: 'Failed to create user' });47 }48});4950// Serve React frontend in production51if (process.env.REPLIT_DEPLOYMENT) {52 app.use(express.static(path.join(__dirname, '../client/dist')));53 app.get('*', (req, res) => {54 res.sendFile(path.join(__dirname, '../client/dist/index.html'));55 });56}5758app.listen(PORT, '0.0.0.0', () => {59 console.log(`Server running on port ${PORT}`);60});Common mistakes when building full-stack apps in Replit
Why it's a problem: Binding Express to localhost or 127.0.0.1, causing 'hostingpid1: an open port was not detected' on deployment
How to avoid: Always use app.listen(PORT, '0.0.0.0', callback). This allows Replit's health check to reach your server.
Why it's a problem: Not adding a catch-all route for React Router, causing 404 errors when refreshing on non-root pages in production
How to avoid: Add app.get('*', ...) after all API routes that sends index.html. This lets React Router handle client-side routing.
Why it's a problem: Running npm install in the root directory when dependencies are split between client and server directories
How to avoid: Run npm install in each directory separately: cd client && npm install and cd server && npm install.
Why it's a problem: Forgetting to add DATABASE_URL to deployment secrets, causing database queries to fail in production
How to avoid: Replit auto-creates DATABASE_URL for the workspace, but you must verify it is also present in the Deployments pane secrets.
Why it's a problem: Using the Vite proxy configuration in production, where Vite is not running
How to avoid: The proxy is a development convenience. In production, serve the built React files from Express using express.static.
Best practices
- Always bind your Express server to 0.0.0.0, not localhost — Replit's health check requires it for deployment
- Use Vite's proxy in development and express.static in production to handle the frontend-backend connection
- Add a catch-all route in Express that serves index.html so React Router works on page refreshes in production
- Store database credentials in Replit's auto-generated environment variables (DATABASE_URL) instead of hardcoding
- Use process.env.REPLIT_DEPLOYMENT to conditionally enable production-only features like static file serving
- Keep API routes prefixed with /api to clearly separate them from frontend routes
- Add deployment secrets separately from workspace secrets — missing secrets are the most common cause of deployment failures
- Use parameterized queries ($1, $2) with pg to prevent SQL injection
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build a full-stack app in Replit with React (Vite) for the frontend and Express for the backend, connected to a PostgreSQL database. Show me the project structure, how to configure the .replit file to run both processes, how to set up a Vite proxy for API requests, and how to serve the React build from Express in production.
Build a full-stack web app with a React frontend using Vite and Tailwind CSS, and an Express backend connected to the PostgreSQL database. Set up React Router with Home, Users, and About pages. Create CRUD API routes for users. Configure the .replit file to run both frontend and backend. Make sure the app works both in development and after deployment.
Frequently asked questions
Yes. Replit Agent supports Next.js with App Router. However, the default React + Tailwind + ShadCN UI stack is the best-supported and produces the most reliable results with Agent. Next.js adds server-side rendering complexity that may cause issues with deployment.
Enable the database in the Cloud tab (click + next to Preview, then Database). Replit auto-creates DATABASE_URL, PGHOST, PGUSER, PGPASSWORD, PGDATABASE, and PGPORT environment variables. Use pg.Pool with process.env.DATABASE_URL to connect.
In development, the Vite proxy handles cross-origin requests. In production, both the frontend and API are served from the same Express server, so CORS is not needed. If you see CORS errors, your proxy configuration may be wrong or you are making requests to the wrong URL.
Use Autoscale deployment for apps with variable traffic. It scales down to zero when idle, keeping costs low. Use Reserved VM if you need WebSockets or background jobs that require an always-on server.
An Autoscale deployment costs $1 per month base plus compute charges. A small app with 50 daily visitors costs approximately $1 to $3 per month. A Reserved VM starts at approximately $10 to $20 per month for always-on hosting.
Yes. RapidDev specializes in building and deploying full-stack applications on Replit, including database design, API architecture, authentication, and deployment configuration for production workloads.
Replit Auth provides zero-setup authentication that works with the click of a button. For custom auth, use a library like Passport.js in Express or integrate a third-party provider like Clerk or Auth0. Store any auth secrets in Tools -> Secrets.
In production, yes — Express serves the React build files on the same port. In development, they run on separate ports (5173 for Vite, 3001 for Express) with Vite's proxy bridging them. This separation enables hot module replacement for faster development.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation