Full-Stack JS Project Setup

· 11 min read · Updated March 20, 2026 · beginner
javascript node web

What You’ll Build

By the end of this tutorial, you will have a working full-stack JavaScript project. It will have:

  • A root project folder with two separate sub-projects inside: backend/ and frontend/
  • A Node.js server (Express) running on localhost:3000 that handles API requests
  • A Vite-powered frontend on localhost:5173 that fetches data from the backend with the click of a button
  • A combined workflow where both servers start together with one command

This is the foundation every full-stack JavaScript app builds on.

Prerequisites: basic JavaScript and command-line knowledge. No prior Node.js or Express experience needed. About 15 minutes.


Project Structure and Folder Organisation

Before writing any code, it helps to understand how a full-stack JavaScript project is organised.

Think of your project as a house with two rooms. Each room has its own utilities, its own furniture, and its own set of instructions. The backend/ folder is one room — it runs on the server and handles data. The frontend/ folder is the other room — it runs in the browser and handles what the user sees.

Each room has its own package.json file. This file keeps track of which packages (libraries) that room depends on. Keeping them separate prevents conflicts — the backend might need Express, while the frontend might need Vite, and those two things never need to know about each other.

Shared files like .gitignore (which tells Git which files to ignore) and README.md (documentation) live at the root level, where they apply to both rooms.

my-fullstack-app/
├── backend/              # Server-side code lives here
│   ├── src/
│   │   └── index.js      # The main server file
│   └── package.json      # Backend dependencies
├── frontend/             # Browser-side code lives here
│   ├── src/
│   │   └── main.js       # The main frontend script
│   ├── index.html
│   └── package.json      # Frontend dependencies
├── .env                  # Secrets and environment settings (not committed to git)
├── .gitignore            # Tells git to ignore node_modules, .env, etc.
└── README.md             # Project documentation
```bash

Notice there is no `node_modules/` folder in the tree. That folder holds all installed packages and should never be committed to version control. Your `.gitignore` file will make sure of that.

### Initialising Git

If you are starting from scratch, initialise a Git repository at the project root:

```bash
git init
git remote add origin <your-repo-url>
```bash

You will commit your code after writing the initial files.

---

## Setting Up the Backend with Express

The backend is a **Node.js server** built with Express. Its job is to receive requests from the frontend, process them, and send back a response. When a user clicks a button in the browser, the frontend sends a request to the backend, which does some work and replies with data.

### Initialising the Backend

Open your terminal and run these commands to create the backend folder and install the necessary packages:

```bash
mkdir backend && cd backend
npm init -y
npm install express cors dotenv
npm install --save-dev nodemon
```bash

Here is what each command does:

- `mkdir backend && cd backend` — creates the folder and moves into it
- `npm init -y` — creates a `package.json` file with default settings
- `npm install express cors dotenv` — installs Express, CORS, and dotenv in one command
- `npm install --save-dev nodemon` — installs nodemon as a dev dependency; it watches your files and restarts the server whenever you save changes

### A Minimal Express Server

Create the file `backend/src/index.js` and add the following code:

```javascript
// backend/src/index.js
const express = require('express');   // Import the Express framework
const cors = require('cors');        // Import CORS middleware
require('dotenv').config();          // Load environment variables from .env

const app = express();               // Create a new Express application
const PORT = process.env.PORT || 3000;  // Use port from .env, or 3000 as fallback

// Middleware — these run before your routes
app.use(cors());                     // Allow cross-origin requests from the frontend
app.use(express.json());             // Parse JSON request bodies automatically

// Routes — define endpoints the frontend can call
// Health check — confirms the server is running
// GET /api/health
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// GET /api/data — responds with a data object
app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from the backend', data: [1, 2, 3] });
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
```bash

To start the server in development mode (with auto-restart on file changes), run:

```bash
npx nodemon src/index.js
```bash

The server will start on `http://localhost:3000`. Leave this terminal open — nodemon will restart the server automatically whenever you save changes to a file.

---

## Setting Up the Frontend with Vite

The frontend is what runs in the user's browser. Vite is a modern build tool that makes frontend development fast. It serves your HTML, CSS, and JavaScript files, and it refreshes the page automatically when you save changes — just like nodemon does for the backend.

### Scaffolding the Frontend

Open a second terminal, go back to the root of your project, and create the frontend:

```bash
cd ..   # Go back to the project root
mkdir frontend && cd frontend
npm create vite@latest . -- --template vanilla
npm install
```bash

`npm create vite@latest . -- --template vanilla` scaffolds a minimal Vite project in the current directory. The `vanilla` template gives you plain JavaScript without any framework. Other templates like `react` or `vue` are available if you prefer those.

### Configuring the Proxy

During development, the frontend runs on `http://localhost:5173` and the backend runs on `http://localhost:3000`. The browser considers these different origins, so it would normally block requests from the frontend to the backend. There are two ways to fix this:

1. Enable CORS on the backend (we already did this in `index.js`)
2. Configure Vite to proxy API requests — so requests to `/api/*` are forwarded to the backend automatically

The proxy approach is cleaner because it means your frontend code can use relative paths like `/api/data` instead of `http://localhost:3000/api/data`, and it works without the browser triggering CORS checks.

Create or update `frontend/vite.config.js`:

```javascript
// frontend/vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      // Any request from the frontend to /api gets forwarded to the backend
      '/api': {
        target: 'http://localhost:3000',  // Backend address
        changeOrigin: true,                // Update the Origin header to match target
      },
    },
  },
});
```html

### The Frontend HTML and JavaScript

Replace the contents of `frontend/index.html` with this:

```html
<!-- frontend/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Full-Stack App</title>
</head>
<body>
  <h1>Full-Stack Demo</h1>
  <button id="fetchBtn">Fetch Backend Data</button>
  <pre id="output">Click the button to see data from the backend</pre>

  <script type="module" src="/src/main.js"></script>
</body>
</html>
```bash

Replace `frontend/src/main.js` with:

```javascript
// frontend/src/main.js
// When the button is clicked, fetch data from the backend
document.getElementById('fetchBtn').addEventListener('click', async () => {
  try {
    // Fetch sends a GET request to /api/data
    // Vite's proxy forwards it to http://localhost:3000/api/data
    const response = await fetch('/api/data');

    // Check if the server responded successfully
    if (!response.ok) {
      throw new Error(`Server error: ${response.status}`);
    }

    // Parse the JSON response
    const result = await response.json();

    // Display the result in the <pre> element
    document.getElementById('output').textContent = JSON.stringify(result, null, 2);
  } catch (error) {
    // If anything goes wrong, show the error message
    console.error('Fetch failed:', error.message);
    document.getElementById('output').textContent = `Error: ${error.message}`;
  }
});
```bash

To start the frontend dev server:

```bash
npm run dev
```bash

The frontend will be available at `http://localhost:5173`. Click the button — it will fetch data from the backend and display it on the page.

---

## Running Both Servers with concurrently

Right now you have two terminals open: one running the backend with nodemon, one running the frontend with Vite. That works, but it is inconvenient. The `concurrently` package lets you run both commands in a single terminal.

Go back to the project root, initialise it as a Node project, and install `concurrently`:

```bash
cd ..
npm init -y
npm install --save-dev concurrently
```txt

Open `package.json` at the root level and update the `scripts` section:

```json
{
  "name": "my-fullstack-app",
  "version": "1.0.0",
  "scripts": {
    "dev": "concurrently --names \"backend,frontend\" --prefix-colors \"blue,green\" \"npm run server\" \"npm run client\"",
    "server": "cd backend && npx nodemon src/index.js",
    "client": "cd frontend && npm run dev"
  }
}
```bash

Now you can start both servers with a single command:

```bash
npm run dev
```javascript

You will see output from both processes in the same terminal, each labelled with its name and a colour so you can tell them apart:

```javascript
[backend] Server running on http://localhost:3000
[frontend]   VITE v5.x.x  ready in 200ms
[frontend]   Local: http://localhost:5173/
```javascript

---

## Environment Variables and Configuration

Environment variables let you store settings that change between environments (development, production) or that should stay private (API keys, database passwords). They live in a `.env` file and get loaded into `process.env` when the server starts.

### Backend Environment Variables

In the `backend/` folder, create a file called `.env`:

```javascript
# backend/.env
PORT=3000
NODE_ENV=development
```javascript

**Important:** Add `.env` to your `.gitignore` file so it never gets committed to version control:

```javascript
# .gitignore
node_modules/
.env
```javascript

In your backend code, `dotenv` loads the `.env` file automatically when you call `require('dotenv').config()`. You can then read any variable with `process.env.VARIABLE_NAME`:

```javascript
// backend/src/index.js (already has this line near the top)
require('dotenv').config();

const PORT = process.env.PORT || 3000;  // Use 3000 if .env doesn't set PORT
console.log(process.env.NODE_ENV);      // Output: development
```javascript

### Frontend Environment Variables

Frontend code runs in the browser, so it does not have access to `process.env`. Vite handles this differently — any variable the frontend should see must be prefixed with `VITE_`.

Create `frontend/.env`:

```javascript
# frontend/.env
VITE_API_URL=/api
```javascript

Access it in your JavaScript with `import.meta.env`:

```javascript
// frontend/src/main.js
const API_URL = import.meta.env.VITE_API_URL || '/api';
console.log(API_URL);  // Output: /api
```bash

Variables without the `VITE_` prefix are not exposed to the frontend. This keeps your backend-only secrets out of the browser.

---

## Common Gotchas and Pitfalls

A few issues come up almost every time someone sets up a full-stack project.

### Port Conflicts

If two services try to use the same port, one will fail. The Express backend defaults to port `3000`, and Vite's dev server defaults to `5173`. Make sure both are different. If something fails to start, check whether another process is already using that port.

### CORS Errors

If you see an error in the browser console like `Access to fetch at 'http://localhost:3000' from origin 'http://localhost:5173' has been blocked by CORS policy`, the backend is not allowing requests from the frontend. Fix this by either:

- Adding `app.use(cors())` to the Express server, or
- Configuring the Vite proxy as described above

### CommonJS vs ES Modules

Node.js defaults to **CommonJS** syntax, which uses `require()` and `module.exports`. If you want to use modern **ES Modules** syntax with `import` and `export`, you need to change your backend code to use ESM imports throughout and add `"type": "module"` to your backend's `package.json`. This is an advanced change — all your `require()` calls must become `import` statements, and import paths must include the file extension. Most new Node.js projects use ES Modules, but beginners typically start with the CommonJS setup shown in this tutorial.

### Missing Dependencies After Cloning

If you clone a project and `npm install` fails with errors like `Cannot find module 'express'`, you forgot to install dependencies. Always run:

```bash
npm install        # Installs root dependencies
cd backend && npm install   # Installs backend dependencies
cd ../frontend && npm install  # Installs frontend dependencies
```javascript

### process.env Is Not Available in the Browser

The `process.env` object is a Node.js feature. Frontend JavaScript runs in the browser, where it does not exist. If you need a variable in the frontend, use Vite's `VITE_` prefix system described above.

---

## Production Build and Next Steps

When you are happy with your app and ready to deploy, the frontend needs to be built into optimised static files. Vite handles this:

```bash
cd frontend
npm run build
```javascript

This creates a `dist/` folder containing minified HTML, CSS, and JavaScript. You can serve these files from your Express server so that the same port serves both the frontend and the API:

```javascript
// In backend/src/index.js, add near the top
const path = require('path');

// Serve the built frontend files
app.use(express.static(path.join(__dirname, '../../frontend/dist')));

// Catch-all route — sends index.html for any route the frontend handles
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
});
```bash

Now the Express server handles both the API and the frontend from a single port.

From here, you could add a database, implement authentication, or build more API routes. The next tutorial in this series covers each of those topics.

---

## See Also

- [Node.js Error Handling Patterns](/tutorials/node-error-handling-patterns/)
- [Node.js Logging and Monitoring](/tutorials/node-logging-and-monitoring/)
- [Node.js Validation and Sanitization](/tutorials/node-validation-and-sanitization/)