# How to build full-stack apps in Replit

- Tool: Replit
- Difficulty: Beginner
- Time required: 30-45 minutes
- Compatibility: Core ($25/month) or Pro ($100/month) plan recommended for PostgreSQL and deployment. Starter plan works for development only.
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
// server/index.js
import express from 'express';
import pg from 'pg';

const app = express();
const PORT = 3001;

app.use(express.json());

// Database connection
const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL
});

// Health check
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Users API
app.get('/api/users', async (req, res) => {
  try {
    const result = await pool.query('SELECT * FROM users ORDER BY id');
    res.json(result.rows);
  } catch (err) {
    console.error('Database error:', err.message);
    res.status(500).json({ error: 'Database query failed' });
  }
});

app.post('/api/users', async (req, res) => {
  const { name, email } = req.body;
  try {
    const result = await pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [name, email]
    );
    res.status(201).json(result.rows[0]);
  } catch (err) {
    console.error('Insert error:', err.message);
    res.status(500).json({ error: 'Failed to create user' });
  }
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});
```

**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.

```
# .replit
entrypoint = "client/src/App.tsx"
modules = ["nodejs-20:v8-20230920-bd784b9"]

# Run both frontend and backend
run = "cd server && node index.js & cd client && npm run dev & wait"

[nix]
channel = "stable-24_05"

[deployment]
run = ["sh", "-c", "cd server && node index.js"]
build = ["sh", "-c", "cd client && npm install && npm run build"]
deploymentTarget = "cloudrun"

[[ports]]
localPort = 5173
externalPort = 80

[[ports]]
localPort = 3001
externalPort = 3001

hidden = [".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.

```
// client/src/App.tsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import Users from './pages/Users';
import About from './pages/About';

function App() {
  return (
    <BrowserRouter>
      <nav className="p-4 bg-gray-100 flex gap-4">
        <Link to="/" className="text-blue-600 hover:underline">Home</Link>
        <Link to="/users" className="text-blue-600 hover:underline">Users</Link>
        <Link to="/about" className="text-blue-600 hover:underline">About</Link>
      </nav>
      <main className="p-4">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/users" element={<Users />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </main>
    </BrowserRouter>
  );
}

export 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.

```
// client/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true
      }
    }
  }
});

// client/src/pages/Users.tsx
import { useEffect, useState } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

export default function Users() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers)
      .catch(console.error);
  }, []);

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">Users</h1>
      <ul className="space-y-2">
        {users.map(user => (
          <li key={user.id} className="p-2 bg-gray-50 rounded">
            {user.name} — {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

**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.

```
// Add to server/index.js — after all API routes
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Serve React build files in production
if (process.env.REPLIT_DEPLOYMENT) {
  app.use(express.static(path.join(__dirname, '../client/dist')));

  // Catch-all: send index.html for client-side routes
  app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, '../client/dist/index.html'));
  });
}
```

**Expected result:** After deployment, visiting any URL on your app loads the React frontend, and /api routes return JSON data from Express.

## Complete code example

File: `server/index.js`

```javascript
// server/index.js — Full-stack Express server
import express from 'express';
import pg from 'pg';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 3001;

app.use(express.json());

// Database connection
const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL
});

// API Routes
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.get('/api/users', async (req, res) => {
  try {
    const result = await pool.query('SELECT * FROM users ORDER BY id');
    res.json(result.rows);
  } catch (err) {
    console.error('Database error:', err.message);
    res.status(500).json({ error: 'Database query failed' });
  }
});

app.post('/api/users', async (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  try {
    const result = await pool.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [name, email]
    );
    res.status(201).json(result.rows[0]);
  } catch (err) {
    console.error('Insert error:', err.message);
    res.status(500).json({ error: 'Failed to create user' });
  }
});

// Serve React frontend in production
if (process.env.REPLIT_DEPLOYMENT) {
  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, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});
```

## Common mistakes

- **Binding Express to localhost or 127.0.0.1, causing 'hostingpid1: an open port was not detected' on deployment** — undefined Fix: Always use app.listen(PORT, '0.0.0.0', callback). This allows Replit's health check to reach your server.
- **Not adding a catch-all route for React Router, causing 404 errors when refreshing on non-root pages in production** — undefined Fix: Add app.get('*', ...) after all API routes that sends index.html. This lets React Router handle client-side routing.
- **Running npm install in the root directory when dependencies are split between client and server directories** — undefined Fix: Run npm install in each directory separately: cd client && npm install and cd server && npm install.
- **Forgetting to add DATABASE_URL to deployment secrets, causing database queries to fail in production** — undefined Fix: Replit auto-creates DATABASE_URL for the workspace, but you must verify it is also present in the Deployments pane secrets.
- **Using the Vite proxy configuration in production, where Vite is not running** — undefined Fix: 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

## Frequently asked questions

### Can I use Next.js instead of React + Express in Replit?

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.

### How do I access my PostgreSQL database?

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.

### Why do I get CORS errors when calling my API?

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.

### What deployment type should I use for a full-stack app?

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.

### How much does hosting a full-stack app on Replit cost?

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.

### Can RapidDev help build production-ready full-stack apps on Replit?

Yes. RapidDev specializes in building and deploying full-stack applications on Replit, including database design, API architecture, authentication, and deployment configuration for production workloads.

### How do I handle authentication in a Replit full-stack app?

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.

### Can I run the frontend and backend on the same port?

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.

---

Source: https://www.rapidevelopers.com/replit-tutorial/how-to-set-up-a-full-stack-javascript-application-on-replit-with-proper-routing
© RapidDev — https://www.rapidevelopers.com/replit-tutorial/how-to-set-up-a-full-stack-javascript-application-on-replit-with-proper-routing
