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.
CORS middleware with an allowlist
Write the factory
corsMiddleware(allowedOrigins)(aSetof origins) that returns a middleware:- if the
Originheader (read viareq.get("origin")) is in the list: setsAccess-Control-Allow-Originto that origin,Access-Control-Allow-Credentialstotrue, andVarytoOrigin(viares.set); - an unknown origin receives no CORS header;
- for an
OPTIONSrequest: setsAccess-Control-Allow-MethodstoGET,POST,PATCH,DELETEandAccess-Control-Allow-HeaderstoContent-Type, then responds204withres.status(204).end()without callingnext; - otherwise calls
next().
function corsMiddleware(allowedOrigins) { // À compléter }- if the
Build API URLs on the client side
Write
apiUrl(base, path, params)that builds the URL of an API call:basecan be""(same origin, Vite proxy) or a URL like"https://api.exemple.fr"(with or without a trailing/);pathstarts with/;params(optional) is an object;undefinedornullvalues are ignored, the others are encoded withURLSearchParams.
Example:
apiUrl("", "/api/notes", { q: "café", page: 2 })gives"/api/notes?q=caf%C3%A9&page=2".function apiUrl(base, path, params = {}) { // À compléter }Same origin?
The client runs on
http://localhost:5173and callshttp://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
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
Is a
VITE_STRIPE_SECRETvariable safe?- Yes, Vite encrypts it
- No, every VITE_ variable is built into the client code and is public
- Yes, if the repository is private
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
- Consuming the API from React: HTTP Client, Loading, and Errors20 min
- Persisting Data: The API and Its SQL Database20 min
- Full-Stack Authentication: Sessions, Cookies, and Protected Routes20 min
- Building and Deploying the Full-Stack Application18 min