What's next for api-client, and what I'd do differently
The last post in the series — where the package is headed, and an honest look at what I'd change if I were starting it over today.
/* Notes on full stack development — frontend, backend, DevOps, and lessons from shipping production software. */
The last post in the series — where the package is headed, and an honest look at what I'd change if I were starting it over today.
Not a generic FAQ — the specific, confusing failures I ran into using this package myself, and what the actual fix turned out to be each time.
A scannable reference for createClient, defineModule, ctx.request, and every config key covered across this series — for when you know what you need and just want the shape.
The mock client swaps in for your real one with the same method signatures, so your components and tests don't know or care the network isn't real.
The same client, wired up correctly in four different environments — and the one Next.js mistake that quietly defeats the RPC bridge from the last two posts.
Deny-by-default is one layer. Here's every other threat the bridge is built to handle — prototype pollution, CSRF, and the line between what it secures and what your code still has to.
Import your normal API client into a client component and check the Network tab. This is the architecture that stops that from happening.
Cache invalidation from the pipeline and TanStack Query's own cache used to be two separate systems that didn't know about each other. Here's how the integration package fixes that.
Generated types describe what the spec said at generation time. This is what catches the gap between that and what the backend actually returns today.
generate, watch, validate, diff — the four CLI commands that keep your client's types honest against a real, changing backend spec.
Adding a trace header to every outgoing request shouldn't mean editing forty methods. Hooks and events are the two different tools for two different jobs.
One BigInt makes JSON.stringify throw, so your logger drops every other field too. What I measured, and a safe() helper that keeps Dates and Errors intact.
Batched logs sit in memory until a timer fires. Serverless freezes your function the moment it responds. Here is what I measured, and the one-line fix.
AbortSignal.timeout() starts counting when you create it, not when fetch runs. In a queue or retry loop it quietly kills requests still waiting their turn.
A network timeout and a 404 are not the same problem and shouldn't be handled with the same catch block. Here's the typed error hierarchy, and when safeMode is worth the tradeoff.
Everyone says console.time() breaks under concurrency because the second call overwrites the first. I tested it in Node, Bun and Deno, and that is not what happens. The second timer is thrown away, the fast request reports the slow request's duration, and the slow request reports nothing at all.
Structured logging only pays off if the data field has a stable shape you can query on. Nothing enforces that by default. Here's how to catch drift with schema validation that never drops the log line, using the Standard Schema spec so it works with Zod, Valibot, or nothing at all.
I set LOG_LEVEL to turn on debug logs for one feature. Nothing appeared. The rules were spelled correctly — they were just in the wrong order. Here is what I measured, and the rule that tells you which order is right.
A 5-per-300ms rate limit let 9 calls through in a 30 millisecond span. Not a bug — the default window type. Here is what I measured, why it happens, and the one option that fixes it.
How to resolve the right tenant ID per request in a shared SSR client — and why ambient context beats passing tenantId through every function call.
When your Redis cache dies, the surprise is that your app stays fast. What breaks is your logging bill. Here is why, and how a circuit breaker fixes it, including two gotchas I found by testing the code.
window.onerror is the answer everyone gives. It never fires for a broken image, and the next library that loads can delete yours. Here is what to use instead, on the client and on the server, tested in a real browser.
A crash at chunks/page.js:1:518 tells you nothing. Source maps turn it back into checkout.ts:3:49. Here's how the lookup works, and the one case where it hands you a real-looking line number that is completely wrong.
AsyncLocalStorage lets you stamp a request ID onto every log line without passing it through every function. But it can fail without throwing a single error — so your logs just quietly lose the ID. Here are the three cases I reproduced, and the two-minute check that catches them.
A component that logs on every render can fire dozens of network requests per second. Here's how per-level throttle, debounce and rate-limit policies fix it — including the one-word config typo that silently brings the flood right back.
Error objects have non-enumerable fields, so a logger that just JSON.stringify's them prints an empty object. Here's why, and a serializer that captures message, stack, cause chains, and AggregateErrors properly.
The problem with console.log in the browser, why a naive 'POST logs to the server' endpoint is a security hole, and how @developerehsan/nextjs-logger solves both with one import.
Four small resilience features that, combined, are the difference between an app that shrugs off a bad network and one that falls apart on a train.
Three cache strategies, why the cache key includes more than the URL, and how to invalidate cleanly after a mutation.
Bearer, cookie, API key, OAuth2 — and the one bug almost every hand-rolled auth layer has: four parallel requests triggering four token refreshes at once.
One flaky endpoint needs a longer timeout. Here's how to give it one without touching your global config or copy-pasting settings everywhere.
One flaky endpoint needs a longer timeout. Here's how to give it one without touching your global config or copy-pasting settings everywhere.
Most methods just return .data and move on. Here's what's actually inside ApiResponse, and how to combine multiple endpoints into one clean method.
Most methods just return .data and move on. Here's what's actually inside ApiResponse, and how to combine multiple endpoints into one clean method.
Before touching config options, understand the client, the modules, and the pipeline — the three ideas everything else in api-client is built on.
A content queue, a Node generator script that calls the Claude API, and a scheduled GitHub Action that writes a post and commits it — the exact autopilot I use to keep this blog fed.
A practical walk through the vercel.json headers block I ship in production — HSTS, X-Frame-Options, X-Content-Type-Options, Permissions-Policy, and immutable long-cache for hashed assets, fonts, and images.
Every project I ship ends up with the same auth-retry-cache logic copy-pasted and slightly broken. Here's why I finally turned that pattern into a real package.
How to build a keyboard-friendly skip link with the visually-hidden-until-focus pattern, and mark up navigation correctly with aria-label and aria-current.
Use @anthropic-ai/sdk with a JSON schema so Claude returns validated, machine-readable data instead of free text — the exact pattern that powers this blog's auto-post generator.
Why the Google Fonts <link> blocks rendering, and how importing @fontsource-variable/jetbrains-mono in your entry file ships the font with your bundle for a faster first paint.
What an OG image is, why it's exactly 1200x630, and how to design one as an SVG template and render it to the PNG that social platforms show in link previews.
Why JavaScript-based theming causes a flash of the wrong theme on static sites, and how baking class="dark" into index.html fixes it before any JS runs.
A beginner-friendly guide to writing a JSON-LD @graph with Person, ProfilePage, WebSite, and FAQPage nodes so search engines understand your portfolio and show rich results.
Tailwind v4 moves configuration into CSS. Here's how to set up @import, a dark variant, and map OKLCH color and radius tokens with @theme inline — with no tailwind.config file at all.
Animations can make some users sick. Here's how to switch them off with prefers-reduced-motion — and the keyframe trick that keeps content visible instead of stuck at width zero.
Recreate the old-monitor / hacker-terminal look with plain CSS: scanline overlays, a clip-path glitch effect, a blinking terminal cursor, and typewriter text — no JavaScript.
How I built a fully prerendered, horizontally-scrolling terminal-themed portfolio with React 19 and vite-react-ssg — and the constraints that shaped every decision, from direct DOM mutation to sr-only crawler mirrors.
Replace the mouse pointer with a glowing dot-and-ring cursor in React by mutating the DOM directly on mousemove, and invert it over any background with mix-blend-mode: difference.
Add left/right swipe navigation to a React slideshow using touchstart and touchend, a distance threshold to ignore accidental taps, and simple direction detection.
Add arrow-key and Home/End navigation to a React slideshow, and learn the one guard that stops it from stealing keystrokes while a visitor is typing in a form.
A mouse wheel fires dozens of events per flick. Learn how to use a ref-based lock plus a timeout so one scroll gesture advances exactly one panel in a React slideshow.
Build a full-viewport horizontal slideshow in React where each section is a panel and navigation moves the whole track sideways with a single translateX on a ref.
Define your validation rules once with Zod, then reuse the exact same schema in your react-hook-form resolver and your serverless API handler. No drift, no duplicated rules.
A typewriter effect that starts from an empty string breaks prerendering and causes hydration mismatches. The fix: seed state with the full text, then replay the typing on the client as pure decoration.
Wire up react-hook-form with a Zod resolver, then make it accessible with aria-invalid, aria-describedby, and an aria-live region — plus a simple status state machine for submit feedback.
Tabs and carousels only render the active item, so crawlers and screen readers miss everything else. The fix: a visually-hidden sr-only block that mirrors the full dataset into the DOM.
A honeypot is a hidden form field humans never see and bots always fill. Here's how I added one to my contact form — the hidden input on the client and the silent 200 on the server.
window, document and localStorage don't exist in Node — so touching them at render time crashes a prerendered build. Here's the rule for keeping React SSG-safe, and the useIsMobile pattern that reads window only inside an effect.
A tiny sliding-window rate limiter that tracks requests per IP in memory and returns 429 when a client sends too many — plus an honest look at why in-memory limits are only a first line of defence on serverless.
A normal React + Vite app ships an empty HTML shell, which is bad for SEO. Here's how to prerender the real content at build time with vite-react-ssg — the single-page entry, the build script, and how hydration takes over.
Send a real email from a Vercel serverless handler using Resend — creating the client from an env-based API key, building the message, escaping user input, and handling the failure cases.
Access tokens leak. Refresh tokens live for weeks. Rotation with reuse detection is what stops a stolen token from becoming a permanent backdoor — here's how I built it in MERN Notes, and how RBAC layers cleanly on top.
Tailwind v4 moves configuration out of JavaScript and into your CSS. Here's how this Astro blog wires up the Vite plugin and defines its whole design system with @theme — no tailwind.config file at all.
Set up Vitest with jsdom and globals, then test three layers: a Zod schema, a React form with Testing Library, and a serverless handler by mocking the Resend constructor.
Astro highlights code with Shiki out of the box. Here's how to pick a theme like github-dark, enable line wrapping, and understand how it plays with custom code-block markup.
A small, dependency-free lightbox that opens images, diagrams, and code full-screen with zoom, drag-to-pan, and keyboard controls. Here's the real client-side logic, piece by piece.
Build one small Astro component that formats dates for humans and machines at once, using Intl.DateTimeFormat and the HTML time element with a datetime attribute.
Add a real comment section to a static Astro blog with zero backend — Giscus stores every comment as a GitHub Discussion. Here's the setup, the config shape, and how to match a dark theme.
Add lightweight, cookie-free analytics to Astro with @vercel/analytics in a tiny reusable component — what it does, when it loads, and how to wire it up.
My blog and my portfolio are separate sites, but they're one person. Here's how a single Person @id in JSON-LD tells search engines exactly that.
How Astro's 404.astro convention works, what makes a helpful not-found page, and how static hosts serve it — walked through with this blog's real 404 page.
Teach search engines what your post actually is. Build a BlogPosting + BreadcrumbList JSON-LD graph in Astro and inject it per-post for rich results.
Learn how Astro's getStaticPaths turns your content into individual static pages at build time, using the real [slug].astro route from this blog.
Ship the three SEO basics every Astro blog needs: an @astrojs/rss feed, an @astrojs/sitemap, and one BaseHead component that emits canonical, Open Graph, Twitter, and robots meta on every page.
Restaurant SaaS, a dating app in two weeks, a five-year-old CRA migration, and leading a startup's frontend. The concrete, unglamorous lessons from moving fast across very different codebases.
Build a complete blog tag system in Astro — a helper that counts tags, a /tags index page, and dynamically generated per-tag pages using getStaticPaths.
Three small helper functions turn an Astro content collection into related-post suggestions and prev/next links — draft hiding, tag-based ranking, and adjacency.
Define an Astro content collection with a Zod schema so every post's frontmatter is validated at build time and fully typed in your editor.
Add fast, fully client-side search to a static Astro blog using Pagefind — no server, no API, and no query ever leaving the visitor's browser.
Astro 7 ships a new default Markdown processor. Here's why you'd override it with unified() to register your own remark and rehype plugins, and how remarkPlugins, rehypePlugins, and shikiConfig fit together.
Centralize every bit of site copy — personal info, experience, skills, projects — into one typed TypeScript data file so components render from a single source of truth instead of hardcoded strings.
Wrap every Shiki code block in a toolbar with a language label, a copy button, and a fullscreen button — emitted at build time by a rehype plugin so there's no layout shift and it works before JavaScript loads.
A tiny CSS keyframe that reveals an image by animating from blurred and over-bright to sharp, with a subtle scale settle — perfect for a load-in or on-screen reveal.
Build a tiny remark plugin that counts the words in a Markdown post and exposes a '4 min read' estimate on Astro's frontmatter — grounded in real, working code.
Animate skill/progress bars from 0 to their target width using a CSS keyframe, a CSS variable for the target, cubic-bezier easing, and forwards fill-mode — driven straight from your data.
Mermaid's internal color math can't parse oklch(), so passing OKLCH tokens straight in gives you washed-out defaults. Here's how I map my OKLCH design tokens to hex and feed them to mermaid.initialize.
Pass an isActive prop down to each section so its entrance animations replay exactly when the panel scrolls into view — driven by a single current index in the parent.
Mermaid is a big library, and most blog posts have zero diagrams. Here's the one-guard pattern I use so only posts that actually contain a diagram download the mermaid JavaScript.
Build a class-based React error boundary that catches render crashes, shows a recovery UI, and stays safe under static prerendering — no browser globals during render.
Astro's built-in Shiki highlighter treats a mermaid fence as code and syntax-highlights it. Here's how a tiny remark plugin rewrites that fence before Shiki sees it, so a client runtime can turn it into a real diagram.