Authentication is a full-stack workflow. The frontend collects credentials and reflects identity state. The backend verifies credentials, creates sessions, and enforces access. Both sides must agree on how the current user is loaded and how expired sessions behave.
Login Flow
type LoginPayload = { email: string; password: string };type CurrentUser = { id: string; email: string; role: "user" | "admin" };async function login(payload: LoginPayload): Promise<CurrentUser> { const res = await fetch("/api/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("Invalid email or password"); return (await res.json()) as CurrentUser;}
With cookie sessions, the browser stores an HTTP-only cookie. JavaScript does not read the cookie directly; it asks /api/me for the current user.
Route Protection
Client route guards improve experience, but server route guards provide security. Protected API routes must check authentication every time.
WARNING
Never rely on client route guards as the only protection. Users can bypass the UI and call APIs directly.
Session Expiration
When /api/me returns 401, the frontend should clear local user state and move to an unauthenticated flow. Avoid infinite retry loops.
TIP
Make auth state boring and centralized. Every screen should not invent its own way to determine whether the user is logged in.
Further Learning
Search these terms to go deeper:
“cookie session authentication full stack” — browser and server session flow
“current user endpoint API” — loading identity on startup
“frontend route guards server authorization” — UX vs security boundaries
End-to-End Auth Flows
Login isn’t just a frontend form or just a backend check — it’s a full-stack conversation. The frontend collects credentials, sends them, and reflects “who’s logged in” throughout the app; the backend verifies them and enforces access on every request.
The login flow
async function login({ email, password }) { const res = await fetch('/api/login', { method: 'POST', credentials: 'include', // lets the browser store the session cookie body: JSON.stringify({ email, password }), }) if (!res.ok) throw new Error('Invalid email or password') return res.json() // the logged-in user's info}
With cookie-based sessions, your JavaScript never touches the actual login cookie — the browser handles that automatically. Instead, your app asks a /api/me endpoint “who am I?” to figure out the current user.
Checking “am I logged in?” when the app loads
When your app starts up, call something like /api/me. If it succeeds, you know who’s logged in. If it returns a 401, treat the user as logged out.
Protecting pages — do it in TWO places
Client-side (hide a page/menu item) — this is just for a nice user experience.
Server-side (check the session on every API call) — this is the actual security.
WARNING
Never rely on the frontend alone to protect a page. Anyone can call your API directly, skipping the UI entirely — the server must check permissions on every request, every time.
Logging out and expired sessions
When a session expires (/api/me starts returning 401), clear the local “logged in” state and send the user back to login — cleanly, without endlessly retrying.
In one sentence
Authentication spans the whole stack: the frontend collects credentials and reflects login state (checking /api/me on load), but real security lives on the server, which must verify the session on every protected request — never trust the frontend alone.
Want to go deeper?
Switch to Expert mode above for CSRF protection with SameSite cookies and centralizing auth state.