8. Sessions & Login
Follow along with code examples here!
In the previous lesson, you built a registration endpoint that hashes passwords with bcrypt. A user can now create an account. But after they create it, how does your server remember them on future requests? This lesson answers that question by introducing sessions — the mechanism that makes login possible.
Table of Contents:
Essential Questions
By the end of this lesson, you should be able to answer these questions:
What does it mean that HTTP is stateless? Why is that a problem for login?
What is a session? What is a cookie? How do they work together?
What does
cookie-sessiondo? What doesreq.sessiongive you access to?What is the login flow, step by step?
What is the
/api/mepattern? Why is it useful for frontends?How do you implement logout using
cookie-session?
Terms
Stateless — HTTP is stateless: every request is independent. The server does not remember previous requests from the same client.
Session — a way for the server to persist information across multiple requests from the same user, typically by storing it server-side and giving the client a token to identify the session.
Cookie — a small piece of data set by a server and automatically sent by the browser with every subsequent request to that domain.
cookie-session— an npm package that stores session data directly in an encrypted, signed cookie (no server-side session store needed).req.session— the session object provided bycookie-session. You can read from and write to it inside any controller or middleware.bcrypt.compare()— a bcrypt function that checks whether a plaintext password matches a stored hash. Returnstrueorfalse./api/me— a convention for an endpoint that returns the currently logged-in user based on the session.
The Problem: HTTP is Stateless
HTTP is a stateless protocol. That means every request your server receives is completely independent — the server has no memory of previous requests.
This creates a fundamental problem for applications that require login. After a user logs in, how does the server know who they are on the next request?
Q: Why doesn't the server just save the user ID in a JavaScript variable after login?
A JavaScript variable in your server code is shared across all users. If you did let currentUser = user after login, you'd overwrite the previous user's session every time anyone logged in. There's no way to associate a server-side variable with a specific browser connection — HTTP doesn't maintain persistent connections between requests.
The Solution: Sessions and Cookies
The solution is sessions: the server stores data about the logged-in user and gives the client a token (in a cookie) to identify themselves on future requests.
How Cookie-Based Sessions Work
The cookie is sent automatically by the browser with every subsequent request to the same domain — no extra frontend code needed. The server reads the cookie, decrypts it, and knows who the user is.
Q: What's the difference between a session stored in a cookie and a session stored in a database?
Cookie-based sessions (what
cookie-sessiondoes): session data is encrypted and stored in the cookie itself. No server-side database needed. Simpler to set up, but there's a size limit on how much data you can store, and invalidating sessions (e.g., forcing a logout) requires extra work since the server doesn't track them.Database-backed sessions (what
express-sessionwith a store does): the server stores session data in a database (or Redis) and only puts a session ID in the cookie. More flexible (no size limit, easy to invalidate), but requires additional infrastructure.
For learning and small apps, cookie-session is sufficient. Production applications often use database-backed sessions.
Setting Up cookie-session
cookie-sessionInstall the package:
Add it to your Express app as middleware, before your routes:
Add SESSION_SECRET to your .env file:
The secret is used to sign the cookie, which prevents tampering. It must be kept private — never commit it to GitHub. Use a long, random string in production.
Once this middleware is in place, req.session is available in every controller. You can read from it and write to it:
Building the Login Endpoint
The Login Flow
Login requires three steps:
Find the user by username (or email) in the database
Compare the submitted password against the stored hash with
bcrypt.compare()If they match, write the user's ID to the session and return the user
The Login Controller
Notice that both "user not found" and "wrong password" return the same 401 Invalid credentials response. This is intentional — telling an attacker whether a username exists gives them information. Always use the same generic message for both cases.
You'll need a model method to find a user by username:
And the route:
The /api/me Auto-Login Pattern
/api/me Auto-Login PatternWhen a user returns to your app after previously logging in, their cookie is still there. The frontend needs a way to ask "Am I still logged in? Who am I?"
The /api/me endpoint handles this:
The frontend calls GET /api/me when the app loads. If the response is a user object, the app knows the user is logged in. If it's a 401, the app shows the login screen.
Q: Why is /api/me useful even after the user just logged in?
When a user logs in on a login page, your app often redirects them to a dashboard or home page. That new page loads fresh — it doesn't "remember" that the login just happened. By calling /api/me on every page load, the app can always determine the current user state from the session, regardless of how the user got to the page.
Logout
Logout is simple — clear the session:
Setting req.session = null tells cookie-session to delete the cookie from the browser. On the next request, req.session.userId will be undefined.
Q: A user logs out, but if they copy their session cookie from before logout and manually paste it back into their browser, can they log back in?
With cookie-session (client-side sessions), yes — in theory. cookie-session doesn't maintain a server-side session store, so it can't invalidate old cookies. Setting req.session = null clears the cookie from the browser, but if someone saved the old cookie value, they could manually restore it.
This is a known limitation of client-side sessions. Mitigations include:
Short
maxAgevalues (sessions that expire quickly)Changing the
secretto invalidate all existing cookies at onceUsing server-side sessions (e.g.,
express-sessionwith a database store) where you can explicitly delete sessions
For learning purposes and most small apps, cookie-session is fine. For production applications handling sensitive data, server-side sessions are more appropriate.
Putting It Together: Auth Endpoints
Here's a summary of the four auth endpoints:
POST
/api/register
Create a new user (hash password, store in DB)
POST
/api/login
Verify credentials, set session cookie
GET
/api/me
Return current user from session (or 401)
DELETE
/api/logout
Clear the session cookie
With these four endpoints, your application has a complete authentication system. The next lesson adds authorization — checking whether a logged-in user has permission to access specific resources.
Q: A user visits your app for the first time. Walk through exactly which auth endpoints get called and when.
App loads → frontend calls
GET /api/meNo session cookie exists yet
Server returns
401andnullFrontend shows the login/register form
User registers → frontend calls
POST /api/registerwith username and passwordServer hashes the password, creates the user in the DB
Server sets
req.session.userIdand returns the new userFrontend shows the logged-in view
User returns the next day → browser automatically sends the session cookie
Frontend calls
GET /api/meServer reads
req.session.userId, looks up the user in the DBReturns the user object
Frontend shows the logged-in view without requiring a new login
User logs out → frontend calls
DELETE /api/logoutServer sets
req.session = nullCookie is cleared from browser
Frontend shows the login form again
Last updated