The Module pattern — or why a kitchen has a pass-through window, not a walk-in door
Nobody eats in the kitchen. Whatever's cooking, whatever's half-chopped on the counter, whatever the recipe says — none of that leaves the room. Only finished plates cross the pass-through window, and that's the only part the dining room ever touches. A JavaScript module works the same way: the file is the kitchen, its top-level state is everything on the counter, and export is the only window cut into the wall. Here it is, in one small, real example.
The issue
Nobody eats in the kitchen. Whatever's half-chopped on the counter, whatever's still in the pan, whatever the recipe says — none of it leaves the room. The only thing that crosses the pass-through window is a finished plate, and that's the only part of the kitchen the dining room ever gets to touch. A JavaScript module works the same way: the file is the kitchen, its top-level variables are the counter, and export is the one window cut into the wall.
Say your app needs an auth token in a handful of unrelated places: the header decides whether to show an avatar or a login button, the API client attaches it to every request, and a route guard redirects to /login when it's missing or expired. All three need to read it. None of them should be able to just reach in and change it.
The naive version
The obvious move — export the state object itself, and let everyone touch it directly:
export const authState = {
token: null,
expiresAt: null,
};
export function login(token, expiresIn) {
authState.token = token;
authState.expiresAt = Date.now() + expiresIn;
}Fix: keep the state, export the API
Move token and expiresAt out of anything exported, and let functions be the only way in or out. Nothing outside this file can name them anymore — it can only call what's exported:
let token = null;
let expiresAt = null;
export function login(newToken, expiresIn) {
token = newToken;
expiresAt = Date.now() + expiresIn;
}
export function logout() {
token = null;
expiresAt = null;
}
export function getToken() {
return isAuthenticated() ? token : null;
}
export function isAuthenticated() {
return token !== null && Date.now() < expiresAt;
}Every other file that cares about auth now goes through the same four functions, whether it's reading or writing:
import { getToken, isAuthenticated, logout } from "./auth-state.js";
if (!isAuthenticated()) redirectToLogin();
fetch("/api/orders", {
headers: { Authorization: `Bearer ${getToken()}` },
});The other half: one file, evaluated once
auth-state.js only ever runs its top-level code once per page load — every file that imports it after that gets the same already-initialized token and expiresAt, not a fresh copy. That's singleton-style sharing without a class or a new anywhere in sight; Issue 012 builds the same guarantee by hand with a class — a module gets it for free, just by being a module.
For more on the plumbing underneath this — live bindings, circular imports, dynamic import() for code-splitting — patterns.dev's Module Pattern writeup is a good next stop.