## services.md
What I write.
- -> API reference documentation
- -> Getting-started & onboarding guides
- -> Tutorials and how-to walkthroughs
- -> README overhauls
- -> Developer-facing feature announcements & changelogs
## work.md
Two real gaps in real, widely-used libraries — found, and filled.
jwt-refresh-tokens-guide.md
- Full login → refresh → protected-route flow, with working code
- Covers algorithm-confusion attacks — a real, current CVE-class vulnerability
- Includes a manual testing checklist before shipping
$ cat jwt-refresh-tokens-guide.md
The jsonwebtoken package is the most widely used JWT library in the Node.js ecosystem, and its documentation is excellent for the basics: signing tokens, verifying them, setting expiration, choosing an algorithm. What it deliberately does not cover is refresh tokens. The maintainers are explicit about this in the README — they consider automatic refresh too easy to get wrong from a security standpoint, so they've left it to each application to implement.
That's a reasonable position for a library maintainer, but it leaves a real gap for anyone building authentication for the first time. This guide fills that gap: a complete, production-oriented pattern for issuing, rotating, and revoking refresh tokens using jsonwebtoken.
The problem with access tokens alone
A JWT access token is stateless — once issued, the server can't invalidate it before it expires. Refresh tokens solve this: a short-lived access token handles regular requests, and a longer-lived refresh token, checked against the server on each use, issues a new access token without asking the user to log in again.
| Access Token | Refresh Token | |
|---|---|---|
| Lifetime | 5–15 min | Days to weeks |
| Sent on | Every API request | Only to the refresh endpoint |
| Server tracks it? | No (stateless) | Yes (so it can be revoked) |
Setting up token generation
const jwt = require('jsonwebtoken');
const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
function generateAccessToken(user) {
return jwt.sign(
{ sub: user.id, role: user.role },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
);
}
function generateRefreshToken(user) {
return jwt.sign(
{ sub: user.id },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d', algorithm: 'HS256' }
);
}
Use two different secrets. If one is ever compromised, the other set of tokens stays safe.
Issuing tokens at login
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await findUserByEmail(email);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
await storeRefreshToken(user.id, refreshToken);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
});
The refresh endpoint: rotation and reuse detection
Two rules make this safe: rotate the refresh token on every use, and detect reuse — if a token that's already been rotated out gets presented again, that's a strong signal it was stolen.
app.post('/refresh', async (req, res) => {
const token = req.cookies.refreshToken;
if (!token) return res.status(401).json({ error: 'No refresh token provided' });
let payload;
try {
payload = jwt.verify(token, REFRESH_TOKEN_SECRET, { algorithms: ['HS256'] });
} catch (err) {
return res.status(403).json({ error: 'Invalid or expired refresh token' });
}
const storedToken = await getStoredRefreshToken(payload.sub);
if (storedToken !== token) {
await revokeAllRefreshTokens(payload.sub);
return res.status(403).json({ error: 'Refresh token reuse detected — please log in again' });
}
const user = await findUserById(payload.sub);
const newAccessToken = generateAccessToken(user);
const newRefreshToken = generateRefreshToken(user);
await storeRefreshToken(user.id, newRefreshToken);
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: newAccessToken });
});
Security pitfalls that actually happen in production
Always pass algorithms explicitly to verify(). This isn't theoretical — it's one of the most common real-world JWT vulnerabilities. Without it, an application can end up trusting whatever algorithm the token's own header claims, letting an attacker forge a token using information that's supposed to be public. This exact class of bug has caused real CVEs across multiple JWT libraries, including early versions of jsonwebtoken itself.
Never store refresh tokens in localStorage. A single XSS vulnerability anywhere in your frontend exposes every logged-in user's refresh token. An httpOnly cookie can't be read by JavaScript at all.
Keep access tokens short-lived (5–15 min), and always rotate refresh tokens — without rotation, one leaked token stays valid for its entire lifetime with no way to detect the leak.
Summary
| Task | Where it happens |
|---|---|
| Short-lived access token | generateAccessToken(), 15 min expiry |
| Rotation on each use | /refresh overwrites the stored token |
| Reuse detection | /refresh compares against the stored token |
| Algorithm pinning | algorithms: ['HS256'] on every verify() call |
react-router-data-routers-guide.md
- Covers the current data router model: loaders, actions, and errorElement
- Surfaces a real window.location timing gotcha not in the official guide
- Includes a quick-reference table mapping old patterns to current ones
$ cat react-router-data-routers-guide.md
React Router moved from a purely component-based routing model (v6 and earlier) to a "data router" model built around loaders and actions, then merged with Remix's architecture entirely. The official docs cover this well if you already know v6 and are migrating. If you're picking up React Router for the first time, that assumption creates a real gap — developers have raised exactly this complaint in the project's own GitHub discussions, saying the guides read like upgrade notes rather than a fresh introduction.
This guide starts from zero. No v6 knowledge assumed.
The core idea: routes own their data
In the older model, a component fetches its own data, usually with useEffect:
function Dashboard() {
const [project, setProject] = useState(null);
useEffect(() => {
fetch('/api/projects/1').then(res => res.json()).then(setProject);
}, []);
if (!project) return <p>Loading...</p>;
return <h1>{project.name}</h1>;
}
The data router model flips this: the route itself declares what data it needs, and React Router fetches it before rendering the component.
Setting up a data router
import { createBrowserRouter, RouterProvider } from 'react-router/dom';
import Root from './routes/root';
import Dashboard, { loader as dashboardLoader } from './routes/dashboard';
import ErrorPage from './routes/error-page';
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: 'dashboard/:projectId',
element: <Dashboard />,
loader: dashboardLoader,
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
Worth calling out if you're coming from v6: RouterProvider is imported from react-router/dom, not react-router-dom. DOM-specific exports were consolidated there in v7, and as of v8, that's the only place RouterProvider lives — react-router-dom is being phased out as anything more than a compatibility shim.
Loaders: fetching data before the component renders
export async function loader({ params }) {
const response = await fetch(`/api/projects/${params.projectId}`);
if (!response.ok) {
throw new Response('Project not found', { status: 404 });
}
return response.json();
}
export default function Dashboard() {
const project = useLoaderData();
return <h1>{project.name}</h1>;
}
useLoaderData() gives you the resolved data directly — no loading state to manage, because React Router doesn't render the component until the loader resolves.
Actions: handling form submissions
import { Form, redirect } from 'react-router';
export async function action({ request }) {
const formData = await request.formData();
const name = formData.get('name');
await fetch('/api/projects', {
method: 'POST',
body: JSON.stringify({ name }),
});
return redirect('/dashboard');
}
export function NewProjectForm() {
return (
<Form method="post">
<input type="text" name="name" required />
<button type="submit">Create Project</button>
</Form>
);
}
The gotcha nobody warns you about: navigation timing
In the old model, window.location and what was actually on screen stayed in sync. In the data router model, during a pending navigation, window.location updates to the new URL before the old route has finished rendering — a component reading it directly can briefly see the destination's parameters while still displaying the previous page.
// Fragile — can momentarily reflect the destination route mid-navigation
const params = new URLSearchParams(window.location.search);
// Correct — always matches the currently rendered route
import { useSearchParams } from 'react-router';
const [searchParams] = useSearchParams();
Summary
| Concept | Old model (v6) | Current model |
|---|---|---|
| Fetching data | useEffect | loader on the route |
| Handling forms | Manual onSubmit | action + <Form> |
| Errors | Component try/catch | errorElement per route |
| Reading the URL | window.location | useParams() / useSearchParams() |
## stack.json
What I work with.
"languages": ["JavaScript", "TypeScript"],
"runtime": ["Node.js"],
"frontend": ["React"],
"writing": ["API docs", "tutorials", "READMEs", "onboarding guides"],
"tools": ["Git", "Markdown", "Notion", "GitBook", "Docusaurus"]
}
Have documentation that needs work?
If your docs are thin, outdated, or nonexistent, I'll read the code, figure out what's actually missing, and write something developers will use.
$ get in touch