8. Sessions & Login

circle-info

Follow along with code examples herearrow-up-right!

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:

  1. What does it mean that HTTP is stateless? Why is that a problem for login?

  2. What is a session? What is a cookie? How do they work together?

  3. What does cookie-session do? What does req.session give you access to?

  4. What is the login flow, step by step?

  5. What is the /api/me pattern? Why is it useful for frontends?

  6. 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 by cookie-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. Returns true or false.

  • /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?

chevron-rightQ: Why doesn't the server just save the user ID in a JavaScript variable after login?hashtag

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.

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.

Install the package:

Add it to your Express app as middleware, before your routes:

Add SESSION_SECRET to your .env file:

circle-exclamation

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:

  1. Find the user by username (or email) in the database

  2. Compare the submitted password against the stored hash with bcrypt.compare()

  3. If they match, write the user's ID to the session and return the user

The Login Controller

circle-info

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

When 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.

chevron-rightQ: Why is /api/me useful even after the user just logged in?hashtag

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.

Putting It Together: Auth Endpoints

Here's a summary of the four auth endpoints:

Method
Endpoint
What it does

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.

chevron-rightQ: A user visits your app for the first time. Walk through exactly which auth endpoints get called and when.hashtag
  1. App loads → frontend calls GET /api/me

    • No session cookie exists yet

    • Server returns 401 and null

    • Frontend shows the login/register form

  2. User registers → frontend calls POST /api/register with username and password

    • Server hashes the password, creates the user in the DB

    • Server sets req.session.userId and returns the new user

    • Frontend shows the logged-in view

  3. User returns the next day → browser automatically sends the session cookie

    • Frontend calls GET /api/me

    • Server reads req.session.userId, looks up the user in the DB

    • Returns the user object

    • Frontend shows the logged-in view without requiring a new login

  4. User logs out → frontend calls DELETE /api/logout

    • Server sets req.session = null

    • Cookie is cleared from browser

    • Frontend shows the login form again

Last updated