ESBuild and Authentication: A Modern Integration Guide
ESBuild has become the go-to bundler for many JavaScript and TypeScript projects because of its extreme speed and minimal configuration. Yet a bundler alone cannot secure your application. Authentication — whether through JSON Web Tokens (JWT), traditional sessions, or OAuth — remains a cornerstone of modern web development. This tutorial shows you how to weave authentication into an application whose build pipeline is powered by ESBuild. We’ll cover practical implementations for both Node.js backends and browser frontends, all bundled with ESBuild.
What Is “ESBuild Authentication”?
The phrase “ESBuild authentication” doesn’t refer to a built-in feature of ESBuild. Instead, it describes the practice of building authentication logic (JWT verification, session middleware, OAuth handlers) inside projects that use ESBuild for their compilation and bundling. Because ESBuild can target Node.js, browser, and even serverless environments, understanding how to bundle authentication code efficiently and securely is essential for modern full‑stack workflows.
Why Authentication Matters in ESBuild-Powered Apps
Authentication controls who can access your APIs and pages. Even the fastest bundler won’t protect you if an unauthenticated user reaches protected resources. Integrating authentication correctly ensures:
- Data protection – only verified users interact with sensitive endpoints.
- Compliance – many regulations (GDPR, HIPAA) require proper access controls.
- User experience – seamless login, persistent sessions, and third‑party login options.
- Maintainability – well‑structured auth code bundled with ESBuild stays fast and modular.
Setting Up Your ESBuild Project
Before diving into authentication, we need a basic project structure. Assume we have a monorepo with a server folder (Node.js API) and a client folder (browser app). We’ll use a single build.mjs script to orchestrate ESBuild.
// build.mjs
import * as esbuild from 'esbuild';
// Bundle server (Node.js target, external dependencies)
await esbuild.build({
entryPoints: ['server/src/index.ts'],
bundle: true,
platform: 'node',
outfile: 'server/dist/bundle.js',
external: ['express', 'jsonwebtoken', 'express-session', 'passport', 'cookie-parser'],
});
// Bundle client (browser target)
await esbuild.build({
entryPoints: ['client/src/main.ts'],
bundle: true,
platform: 'browser',
outfile: 'client/dist/bundle.js',
define: { 'process.env.API_URL': '"http://localhost:3000"' },
});
This script bundles server and client separately. We keep authentication libraries external on the server so they aren’t inlined; ESBuild will simply require them at runtime.
Implementing JWT Authentication
Backend: Generating and Verifying JWTs
We’ll create a simple Express server that issues JWTs on login and protects routes with a verification middleware.
// server/src/auth/jwt.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
const SECRET = process.env.JWT_SECRET || 'change-me-in-production';
export interface JwtPayload {
userId: string;
role: string;
}
export function generateToken(payload: JwtPayload): string {
return jwt.sign(payload, SECRET, { expiresIn: '1h' });
}
export function verifyToken(token: string): JwtPayload {
return jwt.verify(token, SECRET) as JwtPayload;
}
// Express middleware
export function authenticateJwt(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or malformed token' });
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyToken(token);
(req as any).user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// server/src/index.ts (partial)
import express from 'express';
import { generateToken, authenticateJwt } from './auth/jwt';
const app = express();
app.use(express.json());
app.post('/login', (req, res) => {
// Validate credentials (placeholder)
const { username, password } = req.body;
if (username === 'admin' && password === 'secret') {
const token = generateToken({ userId: '1', role: 'admin' });
return res.json({ token });
}
res.status(401).json({ error: 'Invalid credentials' });
});
app.get('/me', authenticateJwt, (req, res) => {
res.json({ user: (req as any).user });
});
app.listen(3000);
After bundling with node build.mjs, the server runs as node server/dist/bundle.js. ESBuild resolves all local imports, leaving only express and jsonwebtoken as external.
Client-Side JWT Handling
The browser client stores the JWT (preferably in memory or a secure httpOnly cookie, but for demonstration we’ll use localStorage) and attaches it to fetch requests.
// client/src/auth.ts
const API = process.env.API_URL;
export async function login(username: string, password: string): Promise {
const res = await fetch(`${API}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) return null;
const data = await res.json();
localStorage.setItem('token', data.token);
return data.token;
}
export async function fetchWithAuth(url: string, options: RequestInit = {}) {
const token = localStorage.getItem('token');
const headers = new Headers(options.headers || {});
if (token) headers.set('Authorization', `Bearer ${token}`);
return fetch(`${API}${url}`, { ...options, headers });
}
// Example usage in main.ts
document.getElementById('login-btn')?.addEventListener('click', async () => {
const token = await login('admin', 'secret');
if (token) {
const res = await fetchWithAuth('/me');
console.log(await res.json());
}
});
The client bundle is a single file that runs in the browser, making authenticated API calls.
Session-Based Authentication
Backend: Express Sessions and Cookie Management
Session authentication relies on a server‑side store and a session cookie. We’ll use express-session with an in‑memory store (suitable for development).
// server/src/auth/session.ts
import session from 'express-session';
import { Request, Response, NextFunction } from 'express';
const SESSION_SECRET = process.env.SESSION_SECRET || 'session-secret-change-me';
export const sessionMiddleware = session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000, // 1 hour
},
});
export function authenticateSession(req: Request, res: Response, next: NextFunction) {
if ((req.session as any)?.userId) {
next();
} else {
res.status(401).json({ error: 'Not authenticated' });
}
}
// server/src/index.ts (session variant)
import express from 'express';
import cookieParser from 'cookie-parser';
import { sessionMiddleware, authenticateSession } from './auth/session';
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(sessionMiddleware);
app.post('/login-session', (req, res) => {
const { username, password } = req.body;
if (username === 'admin' && password === 'secret') {
(req.session as any).userId = '1';
(req.session as any).role = 'admin';
return res.json({ message: 'Logged in' });
}
res.status(401).json({ error: 'Invalid credentials' });
});
app.get('/me-session', authenticateSession, (req, res) => {
res.json({ userId: (req.session as any).userId, role: (req.session as any).role });
});
app.post('/logout-session', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
Because express-session is marked as external in the ESBuild config, it won’t be bundled. The session cookie (connect.sid) is handled automatically by the browser.
Client-Side Considerations for Sessions
For session‑based auth, the client doesn’t need to manually attach tokens. Cookies are sent automatically with every request. The client code can simply call /me-session with fetch (credentials: 'include' if cross‑origin). ESBuild bundles the client normally.
// client/src/sessionClient.ts
const API = process.env.API_URL;
export async function checkSession() {
const res = await fetch(`${API}/me-session`, { credentials: 'include' });
return res.ok ? await res.json() : null;
}
OAuth Integration (Google OAuth Example)
Backend: Passport.js Strategy
OAuth allows users to log in with third‑party providers. We’ll implement Google OAuth2 using passport and passport-google-oauth20.
// server/src/auth/oauth.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { Request, Response, NextFunction } from 'express';
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID!;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET!;
const CALLBACK_URL = process.env.CALLBACK_URL || 'http://localhost:3000/auth/google/callback';
passport.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: CALLBACK_URL,
}, (accessToken, refreshToken, profile, done) => {
// Find or create user based on profile.id
const user = { id: profile.id, displayName: profile.displayName };
return done(null, user);
}));
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
passport.deserializeUser((id: string, done) => {
// Fetch user from database (placeholder)
done(null, { id, displayName: 'Google User' });
});
export function ensureAuthenticated(req: Request, res: Response, next: NextFunction) {
if (req.isAuthenticated()) return next();
res.status(401).json({ error: 'Not authenticated' });
}
// server/src/index.ts (OAuth routes)
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import { ensureAuthenticated } from './auth/oauth';
const app = express();
app.use(session({ secret: 'oauth-secret', resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard'); // or send token/JSON
}
);
app.get('/me-oauth', ensureAuthenticated, (req, res) => {
res.json({ user: req.user });
});
When bundling with ESBuild, keep passport, passport-google-oauth20, and express-session external. Environment variables for OAuth secrets should never be bundled into the code; define them at runtime.
Frontend: Initiating OAuth Flow
The client typically redirects the user to /auth/google. After the callback, the server can issue a JWT or establish a session. The client then behaves like a standard authenticated app.
// client/src/oauthLogin.ts
const API = process.env.API_URL;
export function loginWithGoogle() {
window.location.href = `${API}/auth/google`;
}
// On the callback page (e.g., dashboard.html) we check the session
export async function fetchOAuthUser() {
const res = await fetch(`${API}/me-oauth`, { credentials: 'include' });
return res.ok ? await res.json() : null;
}
The frontend bundle includes this logic, compiled by ESBuild alongside your main application code.
Best Practices for Authentication in ESBuild Projects
- Keep secrets out of bundles – use environment variables and
externalfor sensitive libraries. Never hard‑code JWT secrets or OAuth keys. - Use secure cookies – set
httpOnly,secure, andsameSiteappropriately. For JWTs, prefer secure, httpOnly cookies overlocalStorageto mitigate XSS. - Separate auth configuration – place authentication logic in its own module (like
auth/) so ESBuild can tree‑shake unused code effectively. - Validate environment early – check that required environment variables exist at startup before bundling logic runs.
- Test authentication flows – write integration tests for login, token refresh, and OAuth callbacks. ESBuild doesn’t affect runtime behavior, so standard testing frameworks apply.
- Handle token refresh – for JWTs, implement silent refresh or sliding sessions to improve user experience.
- Bundle only what you need – use entry points that import only active authentication strategies to keep the final bundle lean.
Conclusion
Integrating JWT, session, and OAuth authentication into an ESBuild‑based project is a matter of structuring your code cleanly and configuring the bundler appropriately. ESBuild’s speed and simplicity make it an excellent companion for modern authentication workflows. By externalising sensitive packages, keeping secrets in environment variables, and bundling client and server code separately, you can build secure, performant applications that protect user data without sacrificing development velocity. Whether you choose stateless JWTs, traditional sessions, or social OAuth, the principles remain the same: design your auth layer as a set of focused modules, let ESBuild handle the rest, and always prioritise security over convenience.