This example shows the complete journey of a login flow, from what happens in the user’s browser to how the backend handles the request. Even though the user only sees a simple form, several steps work together behind the scenes: React stores the typed values, the browser sends them to the backend using fetch, the server checks the credentials, and if they match, it returns a token that the browser saves. Below is the full code for both the frontend and backend, along with the folder structure they live in.


Frontend: This component displays the form, collects what the user types, and sends it to the backend when the login button is pressed.

import { useState } from "react";

export default function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const login = async (e) => {
    e.preventDefault();
    const res = await fetch("/login", {
      method: "POST",
      headers: { "Content-Type":"application/json" },
      body: JSON.stringify({ email, password })
    });

    const data = await res.json();
    localStorage.setItem("token", data.token);
    window.location = "/";
  };

  return (
    <form onSubmit={login}>
      <h3>Login</h3>
      <input placeholder="Email" onChange={e => setEmail(e.target.value)} />
      <br />
      <input type="password" placeholder="Password" onChange={e => setPassword(e.target.value)} />
      <br />
      <button>Login</button>
    </form>
  );
}

Backend: This code listens for requests on /login, checks the credentials, and responds with a token if they match the test user.

import express from "express";

const app = express();
app.use(express.json());

// Fake test user (like a tiny in-memory DB)
const user = {
  email: "[email protected]",
  password: "1234"
};

// Login endpoint
app.post("/login", (req, res) => {
  const { email, password } = req.body;

  // Check if email+password match our fake user
  if (email === user.email && password === user.password) {
    return res.json({ token: "FAKE_TOKEN_ABC123" });
  }

  // If not valid:
  res.status(401).json({ error: "Invalid credentials" });
});

app.listen(3000, () => console.log("Server running on <http://localhost:3000>"));

1️⃣ The user opens /login in the browser

React loads the login page and shows the form.

At this point, nothing has been sent to the server — the data only lives in the browser.

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

2️⃣ The user types in email and password

Every keystroke updates React state. The server still has no idea what the user typed yet — it’s only stored in the browser.

<input placeholder="Email" onChange={e => setEmail(e.target.value)} />
<input type="password" onChange={e => setPassword(e.target.value)} />

3️⃣ The user clicks “Login”

When the button is pressed, React intercepts the form submit. Instead of refreshing the page, it runs the login function.

const login = (e) => {
  e.preventDefault(); // stop page reload
  // next step: send data to the server
};

4️⃣ The browser sends an HTTP request to the backend