Skip to main content
RapidDev - Software Development Agency
replit-tutorial

How to build full-stack apps in Replit

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.

What you'll learn

  • Structure a full-stack project with separate client and server directories
  • Configure .replit to run React and Express in parallel
  • Set up Express API routes and React Router for client-side navigation
  • Connect to Replit's built-in PostgreSQL database using environment variables
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read30-45 minutesCore ($25/month) or Pro ($100/month) plan recommended for PostgreSQL and deployment. Starter plan works for development only.March 2026RapidDev Engineering Team
TL;DR

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

1

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.

2

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.

typescript
1// server/index.js
2import express from 'express';
3import pg from 'pg';
4
5const app = express();
6const PORT = 3001;
7
8app.use(express.json());
9
10// Database connection
11const pool = new pg.Pool({
12 connectionString: process.env.DATABASE_URL
13});
14
15// Health check
16app.get('/api/health', (req, res) => {
17 res.json({ status: 'ok', timestamp: new Date().toISOString() });
18});
19
20// Users API
21app.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});
30
31app.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});
44
45app.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.

3

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.

typescript
1# .replit
2entrypoint = "client/src/App.tsx"
3modules = ["nodejs-20:v8-20230920-bd784b9"]
4
5# Run both frontend and backend
6run = "cd server && node index.js & cd client && npm run dev & wait"
7
8[nix]
9channel = "stable-24_05"
10
11[deployment]
12run = ["sh", "-c", "cd server && node index.js"]
13build = ["sh", "-c", "cd client && npm install && npm run build"]
14deploymentTarget = "cloudrun"
15
16[[ports]]
17localPort = 5173
18externalPort = 80
19
20[[ports]]
21localPort = 3001
22externalPort = 3001
23
24hidden = [".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.

4

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.

typescript
1// client/src/App.tsx
2import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
3import Home from './pages/Home';
4import Users from './pages/Users';
5import About from './pages/About';
6
7function 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}
25
26export default App;

Expected result: Clicking navigation links switches between pages without a full reload. The URL bar updates to reflect the current route.

5

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.

typescript
1// client/vite.config.ts
2import { defineConfig } from 'vite';
3import react from '@vitejs/plugin-react';
4
5export 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: true
14 }
15 }
16 }
17});
18
19// client/src/pages/Users.tsx
20import { useEffect, useState } from 'react';
21
22interface User {
23 id: number;
24 name: string;
25 email: string;
26}
27
28export default function Users() {
29 const [users, setUsers] = useState<User[]>([]);
30
31 useEffect(() => {
32 fetch('/api/users')
33 .then(res => res.json())
34 .then(setUsers)
35 .catch(console.error);
36 }, []);
37
38 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.

6

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.

typescript
1// Add to server/index.js — after all API routes
2import path from 'path';
3import { fileURLToPath } from 'url';
4
5const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7// Serve React build files in production
8if (process.env.REPLIT_DEPLOYMENT) {
9 app.use(express.static(path.join(__dirname, '../client/dist')));
10
11 // Catch-all: send index.html for client-side routes
12 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

server/index.js
1// server/index.js — Full-stack Express server
2import express from 'express';
3import pg from 'pg';
4import path from 'path';
5import { fileURLToPath } from 'url';
6
7const __dirname = path.dirname(fileURLToPath(import.meta.url));
8const app = express();
9const PORT = process.env.PORT || 3001;
10
11app.use(express.json());
12
13// Database connection
14const pool = new pg.Pool({
15 connectionString: process.env.DATABASE_URL
16});
17
18// API Routes
19app.get('/api/health', (req, res) => {
20 res.json({ status: 'ok', timestamp: new Date().toISOString() });
21});
22
23app.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});
32
33app.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});
49
50// Serve React frontend in production
51if (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}
57
58app.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.

ChatGPT Prompt

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.

Replit Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.