Full-Stack Architecture: React Client, Node API, and CORS

First lesson of the Full-Stack React + Node Application course, free to read: about 18 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Organize a full-stack project with a React client (Vite) and an Express API
  • Make the client and server communicate in development using the Vite proxy
  • Understand and configure CORS when the client and API are on different origins

Full-stack architectureDevelopment proxyCORSAPI contract

A full-stack application combines two programs:

  • the client: a React application (built with Vite) that runs in the browser;
  • the server: an Express API that runs on Node.js and accesses the database.

They share neither memory nor variables: they communicate only through HTTP requests exchanging JSON. The set of routes, formats, and error codes forms the API contract; documenting it (README, OpenAPI) or sharing it as schemas prevents many misunderstandings.

Organizing the repository

mon-app/
  package.json          shared scripts (npm workspaces)
  client/               React + Vite
    src/App.jsx
    vite.config.js
  server/               Express 5
    src/app.js
    src/server.js
{
  "private": true,
  "workspaces": ["client", "server"],
  "scripts": {
    "dev": "npm run dev -w server & npm run dev -w client",
    "build": "npm run build -w client",
    "test": "npm test -w server"
  }
}

In development, two servers run: Vite on http://localhost:5173 (hot reload) and the API on http://localhost:3000.

The Vite proxy

For the browser, localhost:5173 and localhost:3000 are two different origins (the port is part of the origin). The simplest approach in development is to ask Vite to relay the /api calls:

// client/vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { "/api": "http://localhost:3000" },  // /api/notes -> http://localhost:3000/api/notes
  },
});

The React code then simply calls fetch("/api/notes"): same origin, no CORS problem. In production, you reproduce the same thing by serving the client and the API from the same domain (lesson 5).

When do you need CORS?

The same-origin policy prevents a script from one origin from reading responses from another. If your client is on app.exemple.fr and the API on api.exemple.fr, the API must explicitly allow it with CORS headers:

const ORIGINES = new Set(["https://app.exemple.fr", "http://localhost:5173"]);

app.use((req, res, next) => {
  const origin = req.get("origin");
  if (origin && ORIGINES.has(origin)) {
    res.set("Access-Control-Allow-Origin", origin);       // echo back the allowed origin
    res.set("Access-Control-Allow-Credentials", "true");  // if cookies are used
    res.set("Vary", "Origin");                            // the cache depends on the origin
  }
  if (req.method === "OPTIONS") {                         // preflight request
    res.set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE");
    res.set("Access-Control-Allow-Headers", "Content-Type");
    return res.status(204).end();
  }
  next();
});

The cors package does the same thing: app.use(cors({ origin: [...], credentials: true })). Two rules: an allowlist of origins, and never Access-Control-Allow-Origin: * with cookies (browsers refuse it anyway). CORS protects users in their browser; it is not an authentication mechanism for the API.

Environment variables on the client side

Vite exposes only variables prefixed with VITE_ to React code (import.meta.env.VITE_API_URL). They are built into the shipped JavaScript: anyone can read them. Secrets (private API key, database password) stay exclusively on the server side.

Key takeaways

Two programs, one HTTP/JSON contract. In development, the Vite proxy avoids CORS; if origins differ in production, configure CORS with an allowlist. No secrets in the client.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 2JavaScript in the browser2 hints

    CORS middleware with an allowlist

    Write the factory corsMiddleware(allowedOrigins) (a Set of origins) that returns a middleware:

    • if the Origin header (read via req.get("origin")) is in the list: sets Access-Control-Allow-Origin to that origin, Access-Control-Allow-Credentials to true, and Vary to Origin (via res.set);
    • an unknown origin receives no CORS header;
    • for an OPTIONS request: sets Access-Control-Allow-Methods to GET,POST,PATCH,DELETE and Access-Control-Allow-Headers to Content-Type, then responds 204 with res.status(204).end() without calling next;
    • otherwise calls next().
    function corsMiddleware(allowedOrigins) {
      // À compléter
    }
  2. CodeDifficulty 1JavaScript in the browser2 hints

    Build API URLs on the client side

    Write apiUrl(base, path, params) that builds the URL of an API call:

    • base can be "" (same origin, Vite proxy) or a URL like "https://api.exemple.fr" (with or without a trailing /);
    • path starts with /;
    • params (optional) is an object; undefined or null values are ignored, the others are encoded with URLSearchParams.

    Example: apiUrl("", "/api/notes", { q: "café", page: 2 }) gives "/api/notes?q=caf%C3%A9&page=2".

    function apiUrl(base, path, params = {}) {
      // À compléter
    }
  3. Multiple choiceDifficulty 11 hint

    Same origin?

    The client runs on http://localhost:5173 and calls http://localhost:3000/api/notes. Is it the same origin?

    • Yes, it is the same domain
    • No, the port differs
    • Yes, because both use HTTP
    • It depends on the browser

End-of-lesson quiz

  1. What is the role of the Vite proxy in development?

    • Compile the Express server
    • Relay /api calls to the Node server to stay on the same origin
    • Encrypt requests
  2. Is a VITE_STRIPE_SECRET variable safe?

    • Yes, Vite encrypts it
    • No, every VITE_ variable is built into the client code and is public
    • Yes, if the repository is private
  3. What should a server respond to an allowed OPTIONS preflight request?

    • The content of the resource
    • The Access-Control-Allow-* headers with a 204 (or 200) status
    • A redirect

The rest of the course

  1. Consuming the API from React: HTTP Client, Loading, and Errors20 min
  2. Persisting Data: The API and Its SQL Database20 min
  3. Full-Stack Authentication: Sessions, Cookies, and Protected Routes20 min
  4. Building and Deploying the Full-Stack Application18 min

Full-Stack React + Node Application · Advanced · 4 h 36 min