Full-stack modern web development — 10 lessons covering JSX, hooks, Express, REST APIs, authentication, and deployment.
JSX is a syntax extension for JavaScript that looks like HTML. It compiles to React.createElement() calls under the hood. Every JSX expression must return a single root element.
const element = <h1>Hello, React!</h1>;
const name = "Mayank";
const greeting = <p>Welcome, {name.toUpperCase()}!</p>;
// JSX must have a single parent — use fragments
const list = (
<>
<li>Item 1</li>
<li>Item 2</li>
</>
);
Components are reusable pieces of UI. They are JavaScript functions that return JSX. Component names must start with a capital letter.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const Card = ({ title, children }) => (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
function App() {
return (
<div className="app">
<Welcome name="Mayank" />
<Card title="React Basics">
<p>Components make UI reusable.</p>
</Card>
</div>
);
}
Props are read-only inputs passed from parent to child. They flow one way — down the component tree. You can destructure them in the function signature.
function UserCard({ name, role = "Developer", avatar }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<span>{role}</span>
</div>
);
}
<UserCard
name="Mayank"
role="Full-Stack Dev"
avatar="/mayank.jpg"
/>
function StatusBar({ isLoggedIn, username }) {
if (!isLoggedIn) {
return <p>Please log in.</p>;
}
return (
<div>
<p>Welcome back, {username}!</p>
{username === "admin" && <span className="badge">Admin</span>}
</div>
);
}
Use Array.map() to render lists. Every list item needs a unique key prop for React to track changes efficiently.
function TodoList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.text} {item.done ? "✅" : "⬜"}
</li>
))}
</ul>
);
}
const todos = [
{ id: 1, text: "Learn JSX", done: true },
{ id: 2, text: "Build components", done: false },
{ id: 3, text: "Master hooks", done: false },
];
<TodoList items={todos} />
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter">
<h2>Count: {count}</h2>
<button onClick={() => setCount(count - 1)}>-</button>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default Counter;
className instead of class, htmlFor instead of for, and inline styles are objects (style={{ color: 'red' }}).useState lets you add state to function components. It returns a pair: the current state value and a function to update it. State updates trigger re-renders.
import { useState } from "react";
function StateDemo() {
const [count, setCount] = useState(0);
const [name, setName] = useState("Mayank");
const [user, setUser] = useState({ name: "Mayank", age: 15 });
const birthday = () => {
setUser((prev) => ({ ...prev, age: prev.age + 1 }));
};
const [items, setItems] = useState(["React", "Node.js"]);
const addItem = (item) => {
setItems((prev) => [...prev, item]);
};
return (
<div>
<p>{name} is {user.age} years old</p>
<button onClick={birthday}>Happy Birthday</button>
<ul>
{items.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
);
}
setItems([...items, newItem]), not items.push(newItem).React events are named in camelCase and passed as JSX attributes. Always use functional updates when the new state depends on the previous state.
function EventDemo() {
const [input, setInput] = useState("");
const [submitted, setSubmitted] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
setSubmitted((prev) => [...prev, input]);
setInput("");
};
const handleKeyDown = (e) => {
if (e.key === "Escape") setInput("");
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type something..."
/>
<button type="submit">Add</button>
<ul>
{submitted.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</form>
);
}
function Dashboard({ user, notifications }) {
const hasNotifs = notifications.length > 0;
return (
<div>
{user ? (
<div className="dashboard">
<h2>Welcome, {user.name}</h2>
{hasNotifs ? (
<ul className="notifications">
{notifications.map((n, i) => (
<li key={i}>{n.message}</li>
))}
</ul>
) : (
<p>No new notifications.</p>
)}
</div>
) : (
<p>Please log in to view your dashboard.</p>
)}
</div>
);
}
function FilterList({ data }) {
const [filter, setFilter] = useState("");
const filtered = data.filter((item) =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<p>Showing {filtered.length} of {data.length} items</p>
<ul>
{filtered.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
function TemperatureCalculator() {
const [temp, setTemp] = useState("");
const [scale, setScale] = useState("celsius");
const celsius = scale === "fahrenheit"
? ((parseFloat(temp) - 32) * 5) / 9
: parseFloat(temp);
const fahrenheit = scale === "celsius"
? (parseFloat(temp) * 9) / 5 + 32
: parseFloat(temp);
return (
<div>
<TemperatureInput
scale="celsius"
value={isNaN(celsius) ? "" : celsius.toFixed(1)}
onChange={setTemp}
onScaleChange={setScale}
/>
<TemperatureInput
scale="fahrenheit"
value={isNaN(fahrenheit) ? "" : fahrenheit.toFixed(1)}
onChange={setTemp}
onScaleChange={setScale}
/>
</div>
);
}
useState return?useEffect handles side effects: data fetching, subscriptions, timers, DOM manipulation. The dependency array controls when it runs.
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
} catch (err) {
console.error("Failed to fetch user:", err);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId]);
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found.</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
[] for mount-only effects.useRef creates a mutable ref that persists across renders. Common uses: accessing DOM elements, storing values that don't trigger re-renders.
import { useRef, useEffect } from "react";
function ChatInput({ onSend }) {
const inputRef = useRef(null);
const messageCount = useRef(0);
useEffect(() => {
inputRef.current.focus();
}, []);
const handleSend = () => {
const text = inputRef.current.value;
if (!text.trim()) return;
messageCount.current += 1;
onSend(text);
inputRef.current.value = "";
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Type a message..." />
<button onClick={handleSend}>Send</button>
<p>Messages sent: {messageCount.current}</p>
</div>
);
}
useContext lets you consume context without prop drilling. Create a context, provide it at the top, and consume it anywhere below.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme} className={`btn btn-${theme}`}>
Current theme: {theme}
</button>
);
}
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
Extract reusable logic into custom hooks — functions that start with use and can call other hooks.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((json) => { if (!cancelled) setData(json); })
.catch((err) => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
import { useReducer } from "react";
const initialState = { items: [], total: 0 };
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM":
return {
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case "REMOVE_ITEM":
return {
items: state.items.filter((_, i) => i !== action.index),
total: state.total - state.items[action.index].price,
};
case "CLEAR":
return initialState;
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
<div>
<h2>Cart ({state.items.length}) — ${state.total.toFixed(2)}</h2>
{state.items.map((item, i) => (
<div key={i}>
{item.name} ${item.price}
<button onClick={() => dispatch({ type: "REMOVE_ITEM", index: i })}>
Remove
</button>
</div>
))}
<button onClick={() => dispatch({ type: "CLEAR" })}>Clear Cart</button>
</div>
);
}
In React, form inputs are controlled by state. The input value comes from state, and every change updates state.
function ControlledForm() {
const [form, setForm] = useState({
name: "", email: "", message: "",
});
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log("Submitted:", form);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="name" value={form.name}
onChange={handleChange} placeholder="Name" required />
<input type="email" name="email" value={form.email}
onChange={handleChange} placeholder="Email" required />
<textarea name="message" value={form.message}
onChange={handleChange} placeholder="Message" rows={4} />
<button type="submit">Send</button>
</form>
);
}
function ValidatedForm() {
const [form, setForm] = useState({ email: "", password: "" });
const [errors, setErrors] = useState({});
const validate = () => {
const errs = {};
if (!form.email) errs.email = "Email is required";
else if (!/\S+@\S+\.\S+/.test(form.email)) errs.email = "Invalid email";
if (!form.password) errs.password = "Password is required";
else if (form.password.length < 8) errs.password = "Min 8 characters";
return errs;
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate();
setErrors(errs);
if (Object.keys(errs).length === 0) {
console.log("Valid submission:", form);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input type="email" name="email" value={form.email}
onChange={handleChange} placeholder="Email"
className={errors.email ? "input-error" : ""} />
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input type="password" name="password" value={form.password}
onChange={handleChange} placeholder="Password"
className={errors.password ? "input-error" : ""} />
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Register</button>
</form>
);
}
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
firstName: "", lastName: "", email: "", phone: "",
});
const updateField = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const next = () => setStep((s) => Math.min(s + 1, 3));
const prev = () => setStep((s) => Math.max(s - 1, 1));
return (
<div className="multi-step">
<div className="step-indicator">Step {step} of 3</div>
{step === 1 && (
<div>
<input value={formData.firstName}
onChange={(e) => updateField("firstName", e.target.value)}
placeholder="First Name" />
<input value={formData.lastName}
onChange={(e) => updateField("lastName", e.target.value)}
placeholder="Last Name" />
</div>
)}
{step === 2 && (
<div>
<input value={formData.email}
onChange={(e) => updateField("email", e.target.value)}
placeholder="Email" />
<input value={formData.phone}
onChange={(e) => updateField("phone", e.target.value)}
placeholder="Phone" />
</div>
)}
{step === 3 && (
<pre>{JSON.stringify(formData, null, 2)}</pre>
)}
<div>
{step > 1 && <button onClick={prev}>Back</button>}
{step < 3 && <button onClick={next}>Next</button>}
{step === 3 && <button onClick={() => console.log(formData)}>Submit</button>}
</div>
</div>
);
}
function FileUpload() {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const handleFile = (e) => {
const selected = e.target.files[0];
if (!selected) return;
setFile(selected);
if (selected.type.startsWith("image/")) {
const reader = new FileReader();
reader.onloadend = () => setPreview(reader.result);
reader.readAsDataURL(selected);
}
};
const upload = async () => {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const data = await res.json();
console.log("Uploaded:", data.url);
};
return (
<div>
<input type="file" onChange={handleFile} accept="image/*" />
{preview && <img src={preview} alt="Preview" style={{ maxWidth: 200 }} />}
{file && <p>{file.name} ({(file.size / 1024).toFixed(1)} KB)</p>}
<button onClick={upload}>Upload</button>
</div>
);
}
useRef). Controlled is preferred for most forms.import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function Home() { return <h1>Home Page</h1>; }
function About() { return <h1>About Page</h1>; }
function Contact() { return <h1>Contact Page</h1>; }
function NotFound() { return <h1>404 — Not Found</h1>; }
import { useParams, Link } from "react-router-dom";
const posts = [
{ id: "1", title: "React Basics", content: "JSX and components..." },
{ id: "2", title: "Node.js Intro", content: "Modules and fs..." },
{ id: "3", title: "Full-Stack Apps", content: "Connecting frontend..." },
];
function PostList() {
return (
<div>
<h1>Blog Posts</h1>
{posts.map((post) => (
<div key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</div>
))}
</div>
);
}
function PostDetail() {
const { id } = useParams();
const post = posts.find((p) => p.id === id);
if (!post) return <p>Post not found.</p>;
return (
<article>
<Link to="/posts">← Back to posts</Link>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
import { Outlet, useLocation } from "react-router-dom";
function DashboardLayout() {
const location = useLocation();
return (
<div className="dashboard">
<aside>
<Link to="/dashboard"
className={location.pathname === "/dashboard" ? "active" : ""}>
Overview
</Link>
<Link to="/dashboard/settings">Settings</Link>
<Link to="/dashboard/profile">Profile</Link>
</aside>
<main>
<Outlet />
</main>
</div>
);
}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
import { useNavigate } from "react-router-dom";
function LoginForm() {
const navigate = useNavigate();
const [error, setError] = useState("");
const handleLogin = async (e) => {
e.preventDefault();
const res = await fetch("/api/login", { method: "POST", body: ... });
if (res.ok) {
navigate("/dashboard");
} else {
setError("Invalid credentials");
}
};
return (
<form onSubmit={handleLogin}>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
{error && <p className="error">{error}</p>}
<button type="submit">Log In</button>
</form>
);
}
function BackButton() {
const navigate = useNavigate();
return <button onClick={() => navigate(-1)}>← Back</button>;
}
import { Navigate, Outlet } from "react-router-dom";
function ProtectedRoute({ isAuthenticated }) {
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
<Routes>
<Route path="/login" element={<LoginForm />} />
<Route element={<ProtectedRoute isAuthenticated={isLoggedIn} />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>
</Routes>
import { useSearchParams } from "react-router-dom";
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get("q") || "";
const page = parseInt(searchParams.get("page") || "1");
const updateSearch = (newQuery) => {
setSearchParams({ q: newQuery, page: "1" });
};
return (
<div>
<input value={query}
onChange={(e) => updateSearch(e.target.value)}
placeholder="Search..." />
<p>Searching for: {query} (page {page})</p>
</div>
);
}
<Routes> instead of <Switch>, and element prop instead of component. Always check your version.:id parameter in a React Router v6 route?// CommonJS (Node.js default)
const fs = require("fs");
const path = require("path");
// ES Modules — add "type": "module" in package.json
import fs from "fs";
import path from "path";
// Exporting
// CommonJS
module.exports = { helper1, helper2 };
// ES Modules
export function helper1() {}
export const helper2 = "value";
import fs from "fs";
import { readFile, writeFile, readdir } from "fs/promises";
// Sync (blocks event loop)
const data = fs.readFileSync("data.json", "utf-8");
const parsed = JSON.parse(data);
// Async with promises
async function readDirectory(dirPath) {
try {
const entries = await readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
console.log(`${entry.isDirectory() ? "📁" : "📄"} ${entry.name}`);
}
} catch (err) {
console.error("Cannot read directory:", err.message);
}
}
async function copyFile(src, dest) {
const content = await readFile(src);
await writeFile(dest, content);
console.log(`Copied ${src} → ${dest}`);
}
fs.mkdirSync("src/components", { recursive: true });
// Stream large files
import { createReadStream, createWriteStream } from "fs";
function streamCopy(src, dest) {
const readStream = createReadStream(src);
const writeStream = createWriteStream(dest);
readStream.pipe(writeStream);
writeStream.on("finish", () => console.log("Done!"));
writeStream.on("error", (err) => console.error("Error:", err));
}
fs.readFileSync blocks the entire event loop. Always prefer async/await or streams for production code.import http from "http";
const server = http.createServer((req, res) => {
const { method, url } = req;
if (method === "GET" && url === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("<h1>Home Page</h1>");
} else if (method === "GET" && url === "/api/data") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Hello from Node.js" }));
} else if (method === "POST" && url === "/api/echo") {
let body = "";
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
res.writeHead(201, { "Content-Type": "application/json" });
res.end(body);
});
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
}
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
path.join("/home", "user", "docs", "file.txt");
// → "/home/user/docs/file.txt"
path.resolve("src", "components", "App.js");
// → Absolute path to App.js from cwd
path.extname("document.pdf"); // → ".pdf"
path.basename("/home/user/app.js"); // → "app.js"
path.dirname("/home/user/app.js"); // → "/home/user"
path.isAbsolute("/home/user"); // → true
path.isAbsolute("src"); // → false
import { EventEmitter } from "events";
class TaskRunner extends EventEmitter {
constructor(tasks) {
super();
this.tasks = tasks;
this.completed = 0;
}
run() {
this.emit("start", this.tasks.length);
for (const task of this.tasks) {
this.executeTask(task);
}
}
executeTask(task) {
setTimeout(() => {
this.completed++;
this.emit("progress", this.completed, this.tasks.length);
if (this.completed === this.tasks.length) {
this.emit("done");
}
}, Math.random() * 1000);
}
}
const runner = new TaskRunner(["Install deps", "Build", "Test", "Deploy"]);
runner.on("start", (count) => console.log(`Starting ${count} tasks...`));
runner.on("progress", (done, total) => console.log(`${done}/${total} complete`));
runner.on("done", () => console.log("All tasks finished!"));
runner.run();
const buf = Buffer.from("Hello, Node.js!");
console.log(buf.toString()); // "Hello, Node.js!"
console.log(buf.toString("hex")); // "48656c6c6f..."
console.log(buf.length); // 15
const buf2 = Buffer.from([72, 101, 108, 108, 111]);
console.log(buf2.toString()); // "Hello"
import { Transform } from "stream";
const upperCaseTransform = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
},
});
process.stdin
.pipe(upperCaseTransform)
.pipe(process.stdout);
console.log(process.env.NODE_ENV); // "development"
console.log(process.env.PORT || 3000);
console.log(process.argv);
// ["node", "server.js", "--port", "8080"]
console.log(process.cwd());
const used = process.memoryUsage();
console.log(`${(used.heapUsed / 1024 / 1024).toFixed(2)} MB heap used`);
process.on("SIGTERM", () => {
console.log("SIGTERM received. Shutting down gracefully...");
server.close(() => process.exit(0));
});
import express from "express";
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
req.requestTime = new Date().toISOString();
console.log(`${req.method} ${req.url} at ${req.requestTime}`);
next();
});
app.get("/", (req, res) => {
res.json({ message: "API is running", time: req.requestTime });
});
app.listen(PORT, () => {
console.log(`Express server on port ${PORT}`);
});
app.get("/users/:id", (req, res) => {
const { id } = req.params;
const { fields } = req.query;
res.json({
userId: id,
requestedFields: fields ? fields.split(",") : [],
});
});
app.get("/posts/:postId/comments/:commentId", (req, res) => {
const { postId, commentId } = req.params;
res.json({ postId, commentId });
});
import { Router } from "express";
const userRouter = Router();
userRouter.get("/", (req, res) => {
res.json({ users: ["Alice", "Bob"] });
});
userRouter.get("/:id", (req, res) => {
res.json({ user: { id: req.params.id } });
});
userRouter.post("/", (req, res) => {
res.status(201).json({ created: req.body });
});
app.use("/api/users", userRouter);
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "No token provided" });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(403).json({ error: "Invalid token" });
}
}
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
app.get("/admin/users", authMiddleware, requireRole("admin"), (req, res) => {
res.json({ users: getAllUsers() });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: err.message || "Internal Server Error",
});
});
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get("/api/users/:id", asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
const err = new Error("User not found");
err.status = 404;
throw err;
}
res.json(user);
}));
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).json({
error: err.message,
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
});
});
import express from "express";
const router = express.Router();
let posts = [
{ id: 1, title: "First Post", body: "Hello world", createdAt: new Date() },
];
let nextId = 2;
// GET /api/posts — list all
router.get("/", (req, res) => {
const { sort, limit } = req.query;
let result = [...posts];
if (sort === "newest") result.sort((a, b) => b.createdAt - a.createdAt);
if (limit) result = result.slice(0, parseInt(limit));
res.json({ count: result.length, data: result });
});
// GET /api/posts/:id — get one
router.get("/:id", (req, res) => {
const post = posts.find((p) => p.id === parseInt(req.params.id));
if (!post) return res.status(404).json({ error: "Post not found" });
res.json(post);
});
// POST /api/posts — create
router.post("/", (req, res) => {
const { title, body } = req.body;
if (!title || !body) {
return res.status(400).json({ error: "title and body are required" });
}
const newPost = { id: nextId++, title, body, createdAt: new Date() };
posts.push(newPost);
res.status(201).json(newPost);
});
// PUT /api/posts/:id — full replace
router.put("/:id", (req, res) => {
const index = posts.findIndex((p) => p.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: "Post not found" });
const { title, body } = req.body;
if (!title || !body) {
return res.status(400).json({ error: "title and body are required" });
}
posts[index] = { ...posts[index], title, body, updatedAt: new Date() };
res.json(posts[index]);
});
// DELETE /api/posts/:id
router.delete("/:id", (req, res) => {
const index = posts.findIndex((p) => p.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: "Post not found" });
const deleted = posts.splice(index, 1)[0];
res.json({ message: "Deleted", deleted });
});
export default router;
// 2xx Success
res.status(200).json(data); // OK
res.status(201).json(created); // Created
res.status(204).send(); // No Content
// 4xx Client Error
res.status(400).json({ error: "Bad request" });
res.status(401).json({ error: "Unauthorized" });
res.status(403).json({ error: "Forbidden" });
res.status(404).json({ error: "Not found" });
res.status(409).json({ error: "Conflict" });
res.status(422).json({ error: "Validation failed" });
res.status(429).json({ error: "Too many requests" });
// 5xx Server Error
res.status(500).json({ error: "Internal server error" });
res.status(502).json({ error: "Bad gateway" });
res.status(503).json({ error: "Service unavailable" });
router.get("/", async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const total = await Post.countDocuments();
const posts = await Post.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
res.json({
data: posts,
pagination: {
page, limit, total,
pages: Math.ceil(total / limit),
},
});
});
import mongoSanitize from "express-mongo-sanitize";
app.use(mongoSanitize());
function sanitizeInput(input) {
if (typeof input === "string") {
return input.trim().replace(/<[^>]*>/g, "");
}
if (Array.isArray(input)) return input.map(sanitizeInput);
if (typeof input === "object" && input !== null) {
return Object.fromEntries(
Object.entries(input).map(([k, v]) => [k, sanitizeInput(v)])
);
}
return input;
}
router.post("/", (req, res) => {
const clean = sanitizeInput(req.body);
});
/api/posts not /api/post), and HTTP methods for actions (GET, POST, PUT, DELETE).import bcrypt from "bcrypt";
const SALT_ROUNDS = 12;
async function hashPassword(plainPassword) {
return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}
async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
async function registerUser(email, password) {
const existingUser = await User.findOne({ email });
if (existingUser) throw new AppError("Email already registered", 409);
const hashedPw = await hashPassword(password);
const user = await User.create({ email, password: hashedPw });
return { id: user.id, email: user.email };
}
async function loginUser(email, password) {
const user = await User.findOne({ email });
if (!user) throw new AppError("Invalid credentials", 401);
const valid = await verifyPassword(password, user.password);
if (!valid) throw new AppError("Invalid credentials", 401);
return user;
}
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const JWT_EXPIRES = "7d";
function generateToken(user) {
return jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES }
);
}
function verifyToken(token) {
return jwt.verify(token, JWT_SECRET);
}
app.post("/api/auth/register", asyncHandler(async (req, res) => {
const { email, password, name } = req.body;
if (!email || !password) {
throw new AppError("Email and password required", 400);
}
const hashedPw = await bcrypt.hash(password, 12);
const user = await User.create({ email, password: hashedPw, name });
const token = generateToken(user);
res.status(201).json({
user: { id: user.id, email: user.email, name: user.name },
token,
});
}));
app.post("/api/auth/login", asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new AppError("Invalid email or password", 401);
}
const token = generateToken(user);
res.json({
user: { id: user.id, email: user.email, name: user.name },
token,
});
}));
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "No token provided" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(403).json({ error: "Invalid or expired token" });
}
}
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: `Role '${req.user.role}' is not authorized`,
});
}
next();
};
}
app.get("/api/profile", authenticate, (req, res) => {
res.json({ user: req.user });
});
app.delete("/api/users/:id", authenticate, authorize("admin"), (req, res) => {
// Only admin can delete users
});
function generateTokens(user) {
const accessToken = jwt.sign(
{ id: user.id, role: user.role },
JWT_SECRET,
{ expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ id: user.id },
REFRESH_SECRET,
{ expiresIn: "7d" }
);
return { accessToken, refreshToken };
}
app.post("/api/auth/login", asyncHandler(async (req, res) => {
const user = await loginUser(req.body.email, req.body.password);
const tokens = generateTokens(user);
res.cookie("refreshToken", tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: tokens.accessToken });
}));
app.post("/api/auth/refresh", (req, res) => {
const token = req.cookies.refreshToken;
if (!token) return res.status(401).json({ error: "No refresh token" });
try {
const decoded = jwt.verify(token, REFRESH_SECRET);
const accessToken = jwt.sign(
{ id: decoded.id },
JWT_SECRET,
{ expiresIn: "15m" }
);
res.json({ accessToken });
} catch (err) {
res.status(403).json({ error: "Invalid refresh token" });
}
});
fullstack-app/
├── client/
│ ├── src/
│ │ ├── components/
│ │ │ ├── LoginForm.jsx
│ │ │ ├── Dashboard.jsx
│ │ │ └── Navbar.jsx
│ │ ├── context/
│ │ │ └── AuthContext.jsx
│ │ ├── hooks/
│ │ │ └── useAuth.js
│ │ ├── App.jsx
│ │ └── main.jsx
│ ├── package.json
│ └── vite.config.js
├── server/
│ ├── middleware/
│ │ └── auth.js
│ ├── routes/
│ │ ├── auth.js
│ │ └── posts.js
│ ├── models/
│ │ └── User.js
│ ├── server.js
│ └── package.json
└── README.md
import express from "express";
import cors from "cors";
import mongoose from "mongoose";
import authRoutes from "./routes/auth.js";
import postRoutes from "./routes/posts.js";
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors({
origin: process.env.CLIENT_URL || "http://localhost:5173",
credentials: true,
}));
app.use(express.json());
mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost/fullstack")
.then(() => console.log("MongoDB connected"))
.catch((err) => console.error("MongoDB error:", err));
app.use("/api/auth", authRoutes);
app.use("/api/posts", postRoutes);
app.get("/api/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date() });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({
error: err.message || "Internal Server Error",
});
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
import { createContext, useContext, useState, useEffect } from "react";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem("token"));
const [loading, setLoading] = useState(true);
useEffect(() => {
if (token) {
fetch("/api/auth/me", {
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => res.json())
.then((data) => { if (data._id) setUser(data); })
.catch(() => { localStorage.removeItem("token"); setToken(null); })
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, [token]);
const login = async (email, password) => {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
localStorage.setItem("token", data.token);
setToken(data.token);
setUser(data.user);
};
const logout = () => {
localStorage.removeItem("token");
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ user, token, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
// client/vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:5000",
},
},
});
// Production: serve React build from Express
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
app.use(express.static(path.join(__dirname, "../client/dist")));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "../client/dist", "index.html"));
});
// .env (gitignored)
// MONGODB_URI=mongodb+srv://...
// JWT_SECRET=your-super-secret-key
// CLIENT_URL=https://yourdomain.com
// NODE_ENV=production
// Render / Railway deploy config
// Build: npm install && cd client && npm install && npm run build
// Start: npm start
.env files. Set environment variables on your hosting platform (Render, Railway, Vercel). Use NODE_ENV=production for optimized builds.