| Layer | Strength |
|---|---|
| React | Fast UI, controls login state, protects routes, stores JWT locally. |
| Express | Perfect for API auth flow. Easy hashing, tokens, middleware. |
| SQLite | Lightweight DB with file-based storage. Secure, fast, zero configuration. |
Together, they create a simple, scalable, secure stack for login systems β ideal for MVPs, prototypes, internal apps, SaaS starters.
1οΈβ£ React Login Form β Sends Credentials
// Login.js
const login = async () => {
const res = await fetch("/login", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({email, password})
});
localStorage.setItem("token", (await res.json()).token);
};
Elaboration: Using fetch, we can make a POST request to the /login route. We use the HTTP POST method to send our email and password inside a JSON payload. Before sending, we call JSON.stringify() to convert the JavaScript object into a JSON string, because the browser and server expect request bodies in string format.
If the credentials are correct, the server responds with a signed JWT (JSON Web Token). This token is signed using a secret key stored on the server, and not visible to the client. That secret is what allows the server to verify incoming requests in the future.
We store the JWT in the browser (for example, in localStorage). On later visits to protected pages, like /profile, we include the token in the request, usually through the Authorization header. The server then reads the token, verifies it using the secret key, and if the token is valid, the user is authenticated and authorized to access that page.
2οΈβ£ Express Login Route β Checks Password + Returns JWT
// server.js
app.post("/login", async (req,res)=>{
const {email, password} = req.body;
db.get("SELECT * FROM users WHERE email=?", [email], (err,user)=>{
if(!user) return res.sendStatus(401);
if(!bcrypt.compareSync(password, user.hash)) return res.sendStatus(403);
res.json({token: jwt.sign({id:user.id}, process.env.JWT_SECRET)});
});
});
Elaboration: When a user makes a login request, we extract the email and password from req.body and use the email to look up the user in the database. If no user is found, we immediately return an HTTP status code 401 (Unauthorized), indicating that the credentials donβt match any existing account.
If the user does exist, we compare the entered password with the hashed password stored in the database. We donβt compare them directly β instead, we hash the entered password (via bcrypt) and verify that it matches the existing hash. If the hashes do not match, we can confidently assume the password is incorrect and respond with 403 (Forbidden), meaning the user is not allowed to proceed.
If both checks pass (user exists + password is valid), we generate a signed JSON Web Token (JWT) using our server-side secret. This token is returned to the browser as the response. The client can then store this token (e.g., in localStorage or a cookie) and include it in future requests to prove identity. The server will verify this token on protected routes to determine if the user is authenticated and authorized.
3οΈβ£ SQLite User Storage β Minimal + Secure Hash
-- users.sql
CREATE TABLE users(
id INTEGER PRIMARY KEY,
email TEXT UNIQUE,
hash TEXT
);
// create user (insert)
db.run("INSERT INTO users(email, hash) VALUES(?,?)",
[email, bcrypt.hashSync(password,10)]
);
Elaboration: Instead of storing user passwords directly as plain text (which is insecure and dangerous), we store only a hashed version of the password. When a user registers, we take the password they entered and run it through bcrypt, which transforms it into a one-way cryptographic hash. This hash cannot be reversed back into the original password.
In the database, the password column does not store the real password; it stores the hashed result. For example:
Plain password β "mypassword123"
Bcrypt hash β "$2b$10$9l7V1fE1Wm.UG2Sx..."