Loading content…
Loading content…
Learn full-stack application architecture with React, Node.js, and PostgreSQL
┌─────────────────┐
│ React Browser │
│ (Vite/CRA) │
└────────┬────────┘
│ HTTP/REST
│
┌────────▼────────────────┐
│ Node.js Backend │
│ (Express/Fastify) │
│ - API Routes │
│ - Business Logic │
│ - Auth/Validation │
└────────┬────────────────┘
│ SQL
│
┌────────▼────────────────┐
│ PostgreSQL Database │
│ - Tables/Schemas │
│ - Data Persistence │
└─────────────────────────┘
// Node.js backend endpoints
GET /api/users → List users
POST /api/users → Create user
GET /api/users/:id → Get user
PUT /api/users/:id → Update user
DELETE /api/users/:id → Delete user
import express from "express";
import { db } from "./db";
const app = express();
app.use(express.json());
// List all users
app.get("/api/users", async (req, res) => {
const users = await db.query("SELECT * FROM users");
res.json(users);
});
// Create user
app.post("/api/users", async (req, res) => {
const { name, email } = req.body;
// Validate
if (!name || !email) {
return res.status(400).json({ error: "Missing fields" });
}
// Insert
const user = await db.query(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
[name, email]
);
res.status(201).json(user);
});
// Get user
app.get("/api/users/:id", async (req, res) => {
const user = await db.query(
"SELECT * FROM users WHERE id = $1",
[req.params.id]
);
if (!user) return res.status(404).json({ error: "Not found" });
res.json(user);
});
app.listen(3001, () => console.log("Server running"));
// React component consuming API
"use client";
import { useEffect, useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
// In local development:
// - Either fetch from the absolute backend URL (http://localhost:3001/api/users)
// - Or configure a proxy in Vite (vite.config.ts) to map "/api" to the backend server.
const res = await fetch("http://localhost:3001/api/users");
const data = await res.json();
setUsers(data);
setLoading(false);
};
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
/api/auth/login.localStorage or sessionStorage.HttpOnly cookie. This hides the token from client-side scripts, protecting it from Cross-Site Scripting (XSS) attacks.localStorage, the frontend manually attaches the token in the Authorization: Bearer <token> header.HttpOnly cookies, the browser automatically attaches the cookie with every request.Senior Developer Wisdom
localStorage is common in tutorial projects, production-ready systems should use HttpOnly and SameSite cookies to protect users from token theft via malicious XSS scripts.Common Pitfall
-- Users table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Posts table
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for performance
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_users_email ON users(email);
// Backend: Return structured errors
app.get("/api/users/:id", async (req, res) => {
try {
const user = await db.query(...);
res.json(user);
} catch (error) {
console.error(error);
res.status(500).json({
error: "Failed to fetch user",
code: "USER_FETCH_ERROR"
});
}
});
// Frontend: Handle different error types
const fetchUser = async (id: string) => {
try {
const res = await fetch(`/api/users/${id}`);
if (res.status === 404) {
throw new Error("User not found");
}
if (!res.ok) {
throw new Error("Failed to fetch user");
}
return res.json();
} catch (error) {
console.error(error);
showErrorMessage("Could not load user");
}
};
Pro Tip
# .env.local
DATABASE_URL=postgres://user:pass@localhost:5432/app
JWT_SECRET=replace-me
API_BASE_URL=http://localhost:3001
Common Pitfall
.env.example to document required variables and keep actual values private.version: "3.9"
services:
web:
build: ./client
ports:
- "3000:3000"
env_file:
- .env.local
api:
build: ./server
ports:
- "3001:3001"
env_file:
- .env.local
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app_password
POSTGRES_DB: app_db
ports:
- "5432:5432"
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 3001
CMD ["npm", "run", "start"]
Common Pitfall
.env files or environment variables in your deployment platform.Full-Stack Development
Marking it complete updates your roadmap progress percentage.