Passport.js is the most widely used authentication middleware for Express, not because it implements auth itself, but because it standardizes over 500 different "strategies" behind one consistent API. This lesson covers its core concepts using the Local strategy.
How Passport.js Is Structured
Passport itself does very little: it defines a common interface, Strategy, authenticate(), serializeUser/deserializeUser, and delegates the actual verification logic to whichever strategy package you install (passport-local, passport-google-oauth20, passport-github2, and hundreds more).
Because every strategy implements the same interface, switching from username/password login to adding "Sign in with Google" later means installing a new strategy package, not rewriting your authentication flow.
These two functions control what actually gets stored in the session versus what gets attached to req.user on later requests. Typically, only the user's ID is stored in the session, and the full user record is re-fetched from the database on each request.
Instead of letting passport.authenticate() handle the response automatically, you can pass a callback for full control over the success/failure response, useful for API-style responses instead of redirects.
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json({ error: info?.message });
req.login(user, (loginErr) => {
if (loginErr) return next(loginErr);
res.json({ user });
});
})(req, res, next);
});
Common Mistakes
Forgetting to register express-session before passport.session(), breaking persistent login.
Storing the entire user object in the session instead of just the ID via serializeUser.
Mixing up passport.authenticate()'s default redirect-based flow with an API that expects JSON responses.
Not handling the info object from a failed strategy callback, losing useful failure context.
Key Takeaways
Passport.js standardizes many authentication methods behind one consistent strategy interface.
passport-local handles traditional username/password login; other strategies handle third-party providers.
serializeUser/deserializeUser control what's stored in the session versus attached to req.user.
A custom callback gives full control over API-style JSON responses instead of Passport's default redirects.
Pro Tip
For pure JSON APIs, always use the custom callback form of passport.authenticate() rather than the default middleware form, the default behavior is built around redirect-based, server-rendered flows.
You now understand Passport.js's strategy-based architecture. Next, learn how third-party login works in the OAuth lesson.