Writing
Notes on things I've worked through and wanted to write down.
-
The query planner: why the same query runs differently on different data.
PostgreSQL's query planner chooses how to execute a query based on table statistics. Understanding that process explains why query performance changes as data changes.
-
Connection pooling math: how to calculate the right pool size.
More connections isn't always better. Database connections are expensive resources, and the optimal pool size has a formula — here's how to work it out.
-
Partial indexes: smaller, faster indexes for filtered queries.
A partial index only indexes rows that match a condition. For queries that always filter on a specific value, a partial index is smaller, faster, and cheaper to maintain.
-
Index-only scans: the PostgreSQL optimization that skips the heap.
When all the columns a query needs are in an index, PostgreSQL can answer the query from the index alone — without touching the table at all.
-
MVCC: how PostgreSQL lets reads and writes coexist.
PostgreSQL's MVCC keeps multiple versions of each row so readers never block writers and writers never block readers. Here's how the mechanism works.
-
Write-ahead logging: the mechanism behind database durability.
WAL is how PostgreSQL guarantees that committed transactions survive crashes. Understanding it explains database performance characteristics and replication.
-
Database sharding: when vertical scaling isn't enough.
Sharding partitions data across multiple database instances. Here's how the partitioning strategies work and what operational complexity they introduce.
-
Optimistic concurrency: handling conflicts without locking rows.
Pessimistic locking serializes access to data and creates contention. Optimistic concurrency lets transactions proceed freely and detects conflicts only when they actually occur.
-
Cursor-based pagination: the implementation that doesn't break under load.
Offset pagination skips rows, which breaks when data changes between pages. Cursor-based pagination uses a stable position marker that stays correct regardless of concurrent writes.
-
The N+1 problem in GraphQL: DataLoader and the batching fix.
Every GraphQL resolver that fetches related data naively creates N+1 database queries. DataLoader solves this with batching and caching within a request.
-
API design review: 10 questions before you call an endpoint done.
A checklist of design decisions that are easy to skip when building an endpoint and painful to change after consumers depend on it.
-
HTTP streaming: chunked transfer and what it enables.
Chunked transfer encoding lets servers send response data before they know the total size. Here's how it works and what patterns it unlocks.
-
Static typing in Java vs TypeScript, a beginner's side by side
Both languages catch bugs before you run the code, but they handle it differently. A simple walkthrough using a small payment example.
-
Long polling: the technique that works when WebSockets don't.
Long polling simulates real-time updates using regular HTTP requests held open until data is available. It's a reliable fallback and sometimes the right primary approach.
-
Server-sent events: one-way streaming without WebSocket complexity.
Server-sent events push data from server to client over a persistent HTTP connection. For many real-time use cases they're simpler and more appropriate than WebSockets.
-
Protocol buffers: binary serialization that's smaller and faster than JSON.
Protocol Buffers encode structured data into a compact binary format. Here's how the encoding works and why it's significantly more efficient than JSON.
-
gRPC: when REST isn't the right protocol.
gRPC trades REST's universality for performance, strict contracts, and bidirectional streaming. Here's what that trade buys you and when it's worth making.
-
WebRTC: peer-to-peer connections from the browser.
WebRTC enables direct browser-to-browser communication for video, audio, and data — without routing media through a server. Here's how the connection setup actually works.
-
Drizzle ORM: the type-safe query builder that stays close to SQL.
Drizzle gives you TypeScript types over your database queries without hiding the SQL. The mental model stays SQL — the ergonomics become TypeScript.
-
Prisma vs raw SQL: what you give up and what you gain.
Prisma trades SQL control for developer ergonomics. Understanding exactly what that trade looks like helps you decide when each approach is appropriate.
-
tRPC: end-to-end type safety without writing an API spec.
tRPC lets your frontend call backend procedures with full TypeScript types — no REST spec, no codegen, no type drift between client and server.
-
Zod: runtime validation that generates TypeScript types.
TypeScript types vanish at runtime. Zod keeps them alive — and turns your schemas into a single source of truth for both validation and types.
-
Form libraries: when Formik, React Hook Form, and none are each correct.
A practical comparison of form handling approaches in React -- uncontrolled forms, React Hook Form, and Formik -- with guidance on when each makes sense.
-
React Query vs RTK Query: the actual differences.
A concrete comparison of TanStack Query and RTK Query -- what each does well, where they diverge, and how to choose.
-
Zustand vs Redux: when the simpler store is the right choice.
What Zustand does differently from Redux, the concrete cases where each is the better fit, and what you actually give up going simpler.
-
RTK Query cache invalidation: the tags system that keeps UI in sync.
How RTK Query's tag-based cache invalidation works, how to configure it, and the patterns for keeping your UI consistent after mutations.
-
Streaming HTML: sending the page before all the data is ready.
How HTTP streaming works with React and Next.js, the role of Suspense boundaries, and why it improves Time to First Byte.
-
Server actions: mutations without an API endpoint.
How Next.js Server Actions work, what they replace, the security model, and when they're the right choice over a dedicated API route.
-
Concurrent React: what startTransition actually does.
How React's concurrent features work, what startTransition is for, and the difference between urgent and non-urgent state updates.
-
React.memo: when it helps and when it makes things worse.
How React.memo works, the cases where it actually improves performance, and the common misuse that wastes memory without helping.
-
Turbopack vs webpack: what's actually faster.
What Turbopack is, how it differs from webpack architecturally, what the benchmarks measure, and where it stands today.
-
Next.js app router: what changed and why it matters.
The key differences between Next.js pages router and app router -- React Server Components, layouts, streaming, and the new data fetching model.
-
Service worker caching: the strategies that make apps load faster.
The five service worker caching strategies, when to use each, and how to implement them with Workbox.
-
CSS containment: the property that isolates layout recalculations.
How the CSS contain property works, what containment types exist, and how content-visibility accelerates rendering of off-screen content.
-
Font loading: the FOUT/FOIT problem and the CSS that fixes it.
Why web fonts cause layout shift and invisible text, and how font-display, preload, and size-adjust solve it.
-
Web performance budgets: setting numbers before you regress past them.
How to define performance budgets, integrate them into CI, and use them to keep Core Web Vitals from degrading over time.
-
Module federation: sharing code between independently deployed frontends.
How Webpack Module Federation works, when micro-frontends make sense, and the pitfalls of sharing code across deployment boundaries.
-
Full-text search: when PostgreSQL is enough and when it isn't.
How PostgreSQL full-text search works, its real limitations, and what Elasticsearch and Typesense add that PostgreSQL can't.
-
A/B testing infrastructure: the backend plumbing for controlled experiments.
How to build reliable experiment assignment, the data pipeline for measuring results, and the guardrails that prevent experiments from breaking production.
-
Secrets management: the patterns that don't leak credentials.
Why .env files are not enough, and the tools and patterns that keep database passwords, API keys, and certificates out of your git history.
-
Zero-trust networking: the security model that assumes breach.
What zero-trust means in practice, why the perimeter model failed, and the concrete controls that implement it.
-
Kubernetes basics: the problem it solves and the concepts you need first.
What Kubernetes actually does, the core abstractions you need to understand, and when it's overkill.
-
Infrastructure as code: managing cloud resources from version control.
What infrastructure as code is, why it matters, and how Terraform and Pulumi approach the problem differently.
-
V8 internals: JIT compilation and what deoptimizes your hot paths.
How V8 moves from interpreted bytecode to optimized machine code, and the specific conditions that cause it to throw that work away.
-
JavaScript engine optimization: the hidden classes that matter.
How V8 and other JS engines optimize objects using hidden classes, and the coding patterns that keep your code on the fast path.
-
Memory management in JavaScript: GC, leaks, and the heap snapshot.
How JavaScript's garbage collector works, what causes memory leaks, and how to find them with a heap snapshot in Chrome DevTools.
-
The browser rendering pipeline: DOM, CSSOM, layout, paint, composite.
How the browser turns HTML and CSS into pixels on screen, and what that means for writing performant web code.
-
Monorepo basics: when one repo actually makes sense.
What monorepos solve, the tooling that makes them workable, and the tradeoffs you accept when you put everything in one place.
-
Dependency audits: finding vulnerabilities before they become your problem.
How npm audit works, what the output means, and a practical workflow for keeping your dependency tree clean.
-
npm scripts: the task runner you already have but probably underuse.
How npm scripts work, the lifecycle hooks available, and patterns that replace external task runners for most projects.
-
Semantic versioning: what the 3 numbers mean and when to bump each.
A practical guide to semver -- MAJOR.MINOR.PATCH -- with real rules for when to increment each and how package managers use it.
-
Backpressure: what happens when your consumer is slower than your producer.
Understanding backpressure in data pipelines, streams, and message queues -- and the patterns that prevent your system from falling over.
-
Event sourcing: storing what happened instead of the current state.
How event sourcing works, why it gives you a complete audit log, and when the complexity is worth it.
-
CQRS: separating reads and writes when they have different needs.
CQRS splits your application into two models — one optimized for commands, one for queries. Here's when that separation earns its complexity.
-
The CAP theorem: what distributed systems can and can't guarantee.
What the CAP theorem actually says, how real databases position themselves, and what this means when choosing a data store.
-
Circuit breakers: stopping a cascade failure before it takes everything down.
How the circuit breaker pattern prevents cascade failures in distributed systems, with a Node.js implementation.
-
Distributed tracing: following a request across multiple services.
How distributed tracing tracks a request through multiple services using trace context propagation, and how to implement it with OpenTelemetry.
-
Observability: logs, metrics, traces and how they differ.
The three pillars of observability -- logs, metrics, and distributed traces -- what each tells you, and how they work together.
-
Connection pooling at scale: why PgBouncer exists.
How PgBouncer pools database connections to handle high concurrency, the three pooling modes, and how to configure it.
-
Database replication: read replicas and why they help.
How PostgreSQL replication works, when read replicas make sense, and how to route reads and writes in your application.
-
Blue-green deployments: zero-downtime releases.
How blue-green deployments eliminate downtime during releases, how to implement them, and what to watch out for.
-
Load balancing: distributing traffic without a single point of failure.
How load balancers distribute traffic across server instances, the algorithms they use, and how health checks keep traffic away from failed instances.
-
HTTP/2 and HTTP/3: what changes for your app.
How HTTP/2 multiplexing and HTTP/3's QUIC transport improve performance, and what these protocol changes mean for your application design.
-
WebAssembly: running near-native code in the browser.
How WebAssembly works, how to compile Rust and C to Wasm, and when it makes sense to reach for it.
-
Content Security Policy: the header that stops injected scripts cold.
How Content Security Policy works, how to write a CSP header, and how to deploy it without breaking your application.
-
The 12-factor app: principles that make deployment not a surprise.
A practical walkthrough of the 12-factor app methodology and how each factor prevents the common failure modes of deployment.
-
Event-driven architecture: the pattern that decouples services.
How event-driven architecture works, when to use it, and how to implement it with a message broker versus direct event emission.
-
GraphQL vs REST: the actual tradeoff, not the hype.
A realistic comparison of GraphQL and REST covering over-fetching, type safety, tooling, caching, and when each approach wins.
-
DNS: the lookup chain and the TTL that affects your deploys.
How DNS resolution works step by step, what TTL means in practice, and why DNS propagation can hold up your deployments.
-
TLS/HTTPS: what happens in the handshake and why it takes time.
A step-by-step look at the TLS handshake, what each round trip accomplishes, and how TLS 1.3 and session resumption reduce the cost.
-
CDN architecture: what edge nodes do and how cache invalidation works.
How CDN edge nodes cache and serve content, the difference between push and pull CDNs, and how cache invalidation propagates.
-
The HTTP cache: Cache-Control, ETags, headers that prevent stale data.
How browser caching and CDN caching work through HTTP headers, and how to configure them correctly for different types of content.
-
Service workers: offline capability without a native app.
How service workers intercept network requests to enable offline functionality, background sync, and push notifications.
-
Web Workers: offloading computation without blocking the main thread.
How Web Workers move CPU-intensive tasks off the main thread, with examples for parsing, image processing, and communication patterns.
-
Database connections in serverless: the problem and the solutions.
Why traditional database connection pooling breaks in serverless environments, and the patterns that actually work.
-
Edge rendering: the latency advantage and the constraints.
How edge rendering works, where it reduces latency, and what you give up compared to traditional server rendering.
-
Static vs dynamic rendering: making the right choice for each page.
The tradeoffs between static and dynamic rendering, what determines the right choice for a given page, and how modern frameworks handle the decision.
-
React Server Components: the mental model, not the hype.
What React Server Components actually are, how they differ from client components, and when to use each one.
-
Code splitting at the route level: loading less on every page.
What code splitting is, how route-level splitting works in React, and how to measure whether it's reducing your initial load.
-
Tree shaking: why your bundle is larger than it should be.
What tree shaking is, how it works, what breaks it, and how to verify it's actually happening in your build.
-
Image optimization: format, lazy loading, and the 5 minutes that pay off.
The image optimization techniques with the best effort-to-impact ratio, with concrete implementation examples.
-
Core Web Vitals: LCP, CLS, INP and the code changes that move the numbers.
What each Core Web Vital measures, why it matters for user experience, and the specific code changes that improve each metric.
-
Accessibility in React: the ARIA patterns that actually matter.
The ARIA attributes and accessibility patterns that have the most impact on screen reader users, with React implementation examples.
-
AI-assisted coding: what it changes and what it doesn't.
A grounded look at what AI coding tools actually change about software development workflows and where the limits are.
-
Fine-tuning vs prompting: when each one is the right tool.
What fine-tuning actually does, when it outperforms prompting, and when prompting is the better choice.
-
LLM cost optimization: tokens, batching, and the settings that matter.
Practical techniques for reducing LLM API costs without degrading output quality.
-
MCP: the protocol that lets AI models use tools.
What the Model Context Protocol is, how it works, and how to build an MCP server that exposes tools to AI models.
-
Multi-agent systems: orchestrating LLMs that call each other.
What multi-agent LLM systems are, the patterns for structuring them, and the practical challenges of building reliable agent pipelines.
-
Prompt injection: the security problem unique to LLM applications.
What prompt injection is, why it is hard to prevent, and the mitigations available for LLM application developers.
-
RAG: retrieval-augmented generation without the buzzwords.
What RAG is, how the pipeline works step by step, and when it is the right architecture for an LLM application.
-
LLM context windows: why length limits change how you design prompts.
What context windows are, how they constrain LLM application design, and the practical patterns for working within and around the limits.
-
Message queues: why you'd send a job to a queue instead of handling inline.
What message queues are, the problems they solve, and when adding a queue is the right architectural decision.
-
Redis for session storage: faster than a database for ephemeral data.
Why Redis is a better session store than a relational database, how to set it up, and what operational concerns to keep in mind.
-
Timeouts and retries: the two settings every HTTP client needs.
Why timeouts and retries are mandatory for any HTTP client in production, how to set them correctly, and how to avoid making things worse.
-
API keys vs OAuth: which auth model to use for which integration.
The mechanics of API keys and OAuth, when each is appropriate, and how to implement them securely.
-
Webhooks: receiving HTTP calls instead of making them.
How webhooks work, how to implement a secure receiver, and the operational challenges involved in handling them reliably.
-
Database transactions: what ACID actually guarantees.
What ACID properties mean in practice, with concrete examples of what breaks when each guarantee is violated.
-
The debugging loop: reproduce, isolate, fix, verify.
A structured approach to debugging that works across languages, systems, and types of bugs.
-
Feature flags: shipping incomplete code without breaking anything.
How feature flags work, the different types, and how to use them to decouple deployment from release.
-
Health check endpoints: what belongs in them.
What health check endpoints should verify, the difference between liveness and readiness, and how to implement them correctly.
-
Structured logs: why JSON beats printf in production.
What structured logging is, why plain text logs break down at scale, and how to implement structured logging in common languages.
-
Idempotency keys: making retries safe.
What idempotency keys are, why they matter for distributed systems, and how to implement them on both the client and server side.
-
Caching strategies: in-memory, HTTP headers, and when each is correct.
A practical guide to the main caching layers available to web applications and the decision logic for choosing between them.
-
The N+1 query problem: how it hides in your ORM.
What the N+1 query problem is, how ORMs generate it invisibly, and the techniques to detect and fix it before it hits production.
-
Preview deployments: every PR reviewable without a meeting.
How preview deployments work, why they replace synchronous review sessions, and how to set them up with modern CI pipelines.
-
Cold starts: what causes the delay and how to reduce it.
What happens during a serverless cold start, why it adds latency, and practical techniques to minimize the impact.
-
Serverless functions on Vercel: the Node model and what it can't do.
How Vercel serverless functions work under the Node.js runtime, including execution model, limits, and what patterns don't fit.
-
Conventional commits: the format that writes your changelog automatically.
How the Conventional Commits specification works, how to enforce it with tooling, and how to generate changelogs automatically.
-
ESLint + Prettier in CI: enforcing style without arguments.
How to configure ESLint and Prettier to work together, and how to run them in CI so style violations block merges.
-
GitHub Actions secrets: env variables that don't end up in your logs.
How GitHub Actions secrets work, how to use them safely in workflows, and common patterns for managing sensitive configuration in CI.
-
Automated Vercel deploys: preview environments without any manual step.
How Vercel's Git integration creates preview deployments automatically, and how to configure and control them.
-
Running tests in CI: the job that catches what code review misses.
Why automated tests in CI catch bugs that code review doesn't, and how to structure a test job that provides real signal.
-
GitHub Actions: the workflow file that runs your tests on every push.
How to write a GitHub Actions workflow that runs your test suite on every push and pull request.
-
Vector search: finding similar content without keyword matching.
How vector databases work, how to query them efficiently, and when to use them instead of traditional search.
-
Embeddings: what they encode and a practical use case.
What embedding vectors represent, how to generate them with the OpenAI API, and how to use them for semantic search.
-
Rate limits and retry logic for AI APIs.
How OpenAI and other AI API rate limits work, and how to build retry logic that handles them without hammering the provider.
-
Caching LLM responses: what to cache, what not to, and the key design.
A practical guide to caching LLM API responses, including what makes a good cache key and when caching backfires.
-
Latency budget in a real-time AI app: where the milliseconds go.
How to break down end-to-end latency in a real-time AI application and identify which hops to optimize first.
-
STT to LLM to TTS: a pipeline where every hop adds latency.
How to architect a speech-to-speech pipeline and where to optimize each stage to minimize end-to-end latency.
-
Text-to-speech: latency, voice selection, and streaming audio back.
How TTS APIs work, how to pick voices, and how to stream audio to the client before the full synthesis is done.
-
Gemini vs OpenAI: the API differences that matter when you use both.
A practical comparison of the Gemini and OpenAI APIs covering authentication, message format, tool calling, and streaming.
-
OpenAI function calling: the feature that makes LLMs do real work.
How OpenAI function calling works, how to define tools, and how to wire the model's output back into your application.
-
Streaming LLM responses: why waiting for the full answer feels broken.
LLMs generate tokens one at a time. Streaming sends them as they're produced instead of waiting for completion. How to implement streaming with the OpenAI API and handle it on the client.
-
JSON mode in the OpenAI API: getting structured output you can actually use.
Getting consistent, parseable JSON from LLMs requires more than asking nicely. JSON mode, structured outputs, and the patterns that make LLM output reliable.
-
Speaker diarization: turning a transcript into 'who said what.'
Transcription gives you text. Diarization adds speaker identity. How diarization works, the tools available, and how to combine it with Whisper output.
-
Chunking audio for transcription: size, overlap, and the timing that matters.
The Whisper API has a 25MB file size limit. For long recordings, chunking is required. How to split audio correctly so transcription quality doesn't suffer at the boundaries.
-
OpenAI Whisper API: what the response actually looks like and how to use it.
A practical look at the Whisper transcription API response format, the verbose JSON mode with timestamps, and how to use the output in real applications.
-
ws vs Socket.io: when the abstraction is worth the overhead.
ws is a minimal WebSocket library. Socket.io adds rooms, auto-reconnection, fallbacks, and events on top. What you get from each and when the tradeoffs make sense.
-
Scaling WebSocket servers: why sticky sessions aren't the only answer.
WebSockets make horizontal scaling harder than stateless HTTP. Sticky sessions are the simple solution. Redis pub/sub is the scalable one. When each approach is right.
-
WebSocket on mobile: what the OS does to your connection in the background.
Mobile operating systems aggressively manage connections when an app is backgrounded. What iOS and Android do to your WebSocket and how to handle it.
-
WebSocket authentication: passing a token without cookies.
WebSocket connections don't support Authorization headers in the browser. The patterns for authenticating WebSocket connections -- query params, first-message auth, and cookie-based approaches.
-
Broadcasting to multiple clients: the pub/sub pattern.
When a WebSocket server needs to send a message to many clients, pub/sub is the natural model. How to implement channels, subscriptions, and broadcasts in Node.js.
-
Heartbeat / ping-pong: detecting dead WebSocket connections.
A WebSocket connection can appear open while the underlying TCP connection is gone. Ping-pong heartbeats detect these zombie connections before they cause problems.
-
Reconnection with exponential backoff: handling flaky networks.
WebSocket connections drop. The reconnection strategy matters: naive immediate reconnection can overload a recovering server. Exponential backoff is the standard solution.
-
Text vs binary frames: choosing the right format for real-time data.
WebSocket supports text and binary frames. When to use each, what the tradeoffs are, and how to handle both on the client and server.
-
The WebSocket handshake: what's happening in that HTTP upgrade.
WebSocket connections start as HTTP requests. The upgrade handshake is the mechanism that switches protocols. What each header means and how the switch works.
-
HTTP keeps closing. WebSocket stays open. Here's why that changes everything.
The fundamental difference between HTTP's request-response model and WebSocket's persistent connection, and why persistent connections unlock real-time applications.
-
React Native performance: the FlatList settings that stop jank.
FlatList is the standard way to render large lists in React Native, but its default settings aren't optimal for all cases. The props that make the biggest performance difference.
-
Background vs foreground on iOS: what happens to your WebSocket.
iOS aggressively suspends background apps to save battery. What that means for WebSocket connections and the strategies for handling the transition.
-
Debugging React Native: the tools that actually tell you what's wrong.
A practical overview of the debugging tools available in React Native -- the built-in debugger, Flipper, React DevTools, and logging strategies that work in production.
-
Reanimated: why React Native animations need a different model.
React Native's built-in Animated API has limitations that Reanimated solves by running animations on the UI thread. The core concepts and when the difference matters.
-
Platform-specific code: .ios.js files vs Platform.OS.
React Native offers two ways to write platform-specific code: file extensions that the bundler resolves automatically, and the Platform API for inline branching.
-
Networking in React Native: the fetch quirks nobody documents.
React Native supports fetch and XMLHttpRequest, but there are platform-specific quirks around SSL, timeouts, and iOS App Transport Security that catch developers off guard.
-
AsyncStorage: client-side persistence without a database.
AsyncStorage is React Native's equivalent of localStorage -- a simple key-value store for persisting data on the device. How it works and when to use it.
-
Microphone permission on iOS: the request flow and the edge cases.
How iOS microphone permissions work, when the system dialog appears, what happens when users deny it, and how to handle all the states in a React Native app.
-
The iOS keyboard covers your input. Here's the fix.
When the iOS keyboard opens, it can cover the TextInput the user is typing into. The correct fix depends on your layout, and there are several approaches.
-
React Navigation: Stack, Tab, Drawer and when each one fits.
The three core navigators in React Navigation, what each one renders natively, and the patterns that work well for real mobile apps.
-
Flexbox on mobile: the defaults that differ from the web.
React Native uses Flexbox for layout, but the defaults are different from the web. The key differences that cause layout confusion and how to work with them.
-
React Native is not React for mobile. Here's what actually changes.
React Native shares React's component model but runs in a fundamentally different environment. The differences that trip up web developers most often.
-
tailwind.config design tokens: one source of truth for colors and spacing.
How to extend Tailwind's config with custom design tokens so your brand colors, spacing scale, and typography are defined once and used everywhere.
-
Dark mode in Tailwind: class strategy vs media query.
Tailwind supports two dark mode strategies. Here is what each one does, how to implement both, and when to choose one over the other.
-
Mobile-first responsive design with Tailwind: the breakpoints that matter.
How Tailwind's breakpoint system works, why mobile-first means unprefixed classes apply to small screens, and which breakpoints to actually use.
-
Why utility-first CSS feels wrong for 3 days and right forever after.
The mental shift required to write Tailwind CSS, why HTML with long class strings looks like a mistake at first, and why it stops feeling that way.
-
Redux DevTools: replaying state to find the exact moment things broke.
A practical guide to Redux DevTools features -- time-travel debugging, action inspection, and state diffing -- that make tracking down state bugs much faster.
-
Normalized state in Redux: why flat beats nested.
Nested arrays in Redux state cause duplication and painful updates. Normalization flattens the structure so updates are simple and lookups are O(1).
-
Selectors in Redux: the layer that keeps components dumb about state shape.
How selector functions decouple your components from your Redux state structure, and why memoized selectors with Reselect belong in every Redux codebase.
-
RTK Query vs useEffect: the comparison that changed how I fetch data.
A side-by-side look at fetching data with useEffect versus RTK Query, and why the latter eliminates entire categories of bugs.
-
Redux thunks: async logic without an extra library.
How createAsyncThunk from Redux Toolkit handles async operations like API calls, manages loading and error states automatically, and integrates with extraReducers.
-
Redux without the boilerplate: what createSlice actually removes.
How createSlice from Redux Toolkit eliminates the action types, action creators, and switch statements that made classic Redux painful to maintain.
-
Virtualizing long lists: rendering 10,000 rows without freezing the browser.
How virtualization works, how to implement it with react-window or TanStack Virtual, and what to consider when your list has variable-height items.
-
React.lazy and Suspense: loading code only when you need it.
How code splitting with React.lazy and Suspense works, how to apply it to routes and heavy components, and what to watch out for.
-
Optimistic UI: updating the screen before the server responds.
How optimistic UI works, how to implement it in React, and how to roll back cleanly when the server request fails.
-
Formik + Yup: form validation that doesn't require 50 lines of state.
How to use Formik for form state management and Yup for declarative validation schemas, replacing boilerplate useState and error-tracking logic with a clean, structured approach.
-
Controlled vs uncontrolled inputs: why mixing them causes unreproducible bugs.
The difference between controlled and uncontrolled form inputs in React, why switching between them causes warnings and bugs, and when to use each approach.
-
Error boundaries: the component that keeps one crash from killing the page.
How error boundaries work, how to implement one, and where to place them so a single component crash doesn't take down the entire React application.
-
React Router nested routes: the layout pattern that removes repeated code.
How to use nested routes in React Router v6 to share layouts across pages, eliminating duplicated navigation and wrapper components.
-
Context API performance: the provider pattern that doesn't re-render everything.
Why Context causes unexpected re-renders, and how to structure your providers to limit re-renders to only the components that actually consume the changed value.
-
Custom hooks: the extraction rule that keeps them actually reusable.
When to extract a custom hook, how to name and structure it for genuine reusability, and the mistakes that make custom hooks more complex than the code they replaced.
-
useRef: the escape hatch that lets you break React's rules safely.
What useRef is for beyond DOM access, when to use it instead of useState, and the situations where mutating a ref is the correct solution.
-
useMemo and useCallback: the optimization you're probably adding too early.
What useMemo and useCallback actually do, when they help, and why adding them everywhere makes your code slower and harder to read.
-
useEffect's dependency array is a contract. Breaking it causes silent bugs.
Why the useEffect dependency array is not a performance optimization but a correctness requirement, and what happens when you leave things out of it.
-
useState anti-patterns that cause re-renders you can't explain.
The most common useState mistakes that create unnecessary re-renders, stale state, and bugs that are hard to trace back to their source.
-
React's core model: UI as a function of state. Everything follows from this.
The single idea behind React's design: UI = f(state). Once you internalize this, useState, re-renders, and component composition all make sense as consequences of one principle.
-
.dockerignore: the file that keeps node_modules out of your image.
What .dockerignore does, why it matters for build speed and image size, and what to put in it for a typical Node.js project.
-
Health checks in docker-compose: making containers wait for each other.
How to use healthcheck and condition: service_healthy to ensure dependent containers only start after their dependencies are actually ready, not just running.
-
Hot reload inside Docker: keeping development fast.
How to get file-watching and hot module replacement working inside a Docker container using bind mounts, so you keep the benefits of containerization without losing developer speed.
-
Container networking: how two containers find each other by name.
A practical look at Docker networks, DNS resolution between containers, and why your API container can't reach 'localhost' from inside another container.
-
Multi-stage Docker builds: the technique that cuts image size by 60%.
Multi-stage builds compile or bundle your application in one stage and copy only the artifacts to a minimal production image, leaving build tools and dev dependencies behind.
-
Docker volumes: why your data disappears and how to make it persist.
Containers have ephemeral storage — data written inside a container is lost when it's deleted. Volumes are how you make data survive container restarts and replacements.
-
docker-compose for Node + MongoDB: local dev without installing anything.
A complete docker-compose setup for a Node.js API and MongoDB that gives every developer an identical local environment without installing Node or MongoDB on their machine.
-
A Dockerfile for a Node app that actually works in production.
A production-ready Node.js Dockerfile with a non-root user, proper signal handling, layer caching optimization, and a minimal base image.
-
Docker images vs containers: the mental model in 3 sentences.
Developers often conflate Docker images and containers. The distinction is simple but important — getting it right makes every other Docker concept easier to understand.
-
Write migrations that can run twice without breaking anything.
Idempotent migrations don't fail if they've already been applied. This property makes deployments safer and CI pipelines more reliable.
-
EXPLAIN ANALYZE: reading a query plan to find the slow part.
EXPLAIN ANALYZE runs a query and shows exactly how PostgreSQL executed it — which indexes were used, how many rows were scanned, and where the time was spent.
-
Full-text search in PostgreSQL: no extra service required.
PostgreSQL's built-in full-text search uses tsvector and tsquery to support ranked, stemmed, stop-word-filtered search without adding Elasticsearch or another service.
-
JSONB columns: flexible schema without breaking your relational model.
PostgreSQL's JSONB type stores structured JSON with binary indexing and full operator support. It adds flexibility for variable attributes without requiring a document database.
-
Window functions in PostgreSQL: running totals without a subquery.
Window functions compute values across rows related to the current row without collapsing them into groups. They're the clean solution to running totals, rankings, and row comparisons.
-
INNER vs LEFT JOIN: the one you reach for 90% of the time and why.
INNER JOIN returns only matching rows. LEFT JOIN returns all rows from the left table. Choosing wrong silently drops data or inflates result sets — here's how to think about it.
-
PostgreSQL indexes: B-tree, partial, composite and which queries need each.
Not every index type is right for every query. PostgreSQL offers B-tree, partial, and composite indexes with different cost profiles. Here's how to match index type to query pattern.
-
Relational modeling: when to normalize and when to stop.
Normalization removes redundancy but adds joins. The practical question is which normal form your schema should target and when denormalization is the right engineering tradeoff.
-
Soft deletes: why you almost never want to actually delete data.
Hard deletes are irreversible and break foreign key references. The soft delete pattern marks records as deleted while keeping them in the database, enabling recovery and audit trails.
-
Atlas Search: full-text search without adding Elasticsearch to your stack.
MongoDB Atlas Search provides Lucene-powered full-text search natively in MongoDB, eliminating the need for a separate Elasticsearch cluster for most search use cases.
-
Schema versioning in MongoDB: migrations without downtime.
MongoDB's flexible schema doesn't eliminate the need for migrations — it changes how you do them. Here's the lazy migration pattern that updates documents without a big-bang script.
-
Mongoose populate vs $lookup: same result, very different performance.
Mongoose's populate() runs multiple queries in application code. $lookup joins in the database. Understanding the difference prevents N+1 query problems at scale.
-
MongoDB transactions: what they cost and when they're worth it.
MongoDB supports multi-document ACID transactions since version 4.0, but they carry overhead. Understand when you genuinely need them and when document modeling eliminates the need.
-
The aggregation pipeline: $match, $group, $project with a real example.
MongoDB's aggregation pipeline processes documents through stages. Walk through a real analytics query using $match, $group, and $project to understand how data flows between stages.
-
MongoDB without indexes is a full collection scan every time.
Without the right indexes, every query MongoDB runs reads every document in the collection. Here's how indexes work, how to create them, and how to verify they're being used.
-
Mongoose validators: catching bad data before it reaches the database.
Mongoose schema validation runs before writes and can be customized beyond built-in types. Here's how built-in, custom, and async validators work and when to use each.
-
Embed vs reference in MongoDB: the decision that's hard to undo.
MongoDB gives you the choice to embed related data or reference it. The wrong choice causes either performance problems or query complexity that's expensive to reverse later.
-
Secrets in env variables: why .env is not enough and what to do instead.
Environment variables are better than hardcoding secrets, but .env files have real risks. Here's the threat model and the patterns that actually protect secrets in production.
-
XSS in React: the one dangerous prop and the headers that block the rest.
React escapes output by default, but one escape hatch can reintroduce XSS. Understand dangerouslySetInnerHTML, when it's legitimate, and the HTTP headers that provide defense in depth.
-
Parameterized queries are the only SQL injection fix that works. No exceptions.
SQL injection persists because developers reach for string sanitization instead of parameterized queries. Here's why sanitization fails and how parameters work at the driver level.
-
CSRF is not just a checkbox. Here's the attack and why tokens stop it.
Walk through a real CSRF attack against a cookie-based session, then understand exactly why the synchronizer token pattern defeats it at the HTTP level.
-
Google sign-in with OAuth 2.0: what happens in those 6 redirects.
Step through the complete OAuth 2.0 authorization code flow that powers Google sign-in, explaining each redirect and what's being exchanged at each step.
-
RBAC: the middleware pattern that scales from 2 roles to 20.
Role-based access control implemented as Express middleware — a clean pattern that keeps authorization logic out of route handlers and scales without becoming unmanageable.
-
bcrypt's cost factor: what changing that number actually does.
The cost factor in bcrypt is not arbitrary — it controls exactly how long hashing takes and why that slowness is the entire point of the algorithm.
-
Refresh tokens: the rotation pattern that lets you kick stolen sessions.
Refresh token rotation solves the core problem with stateless JWTs: you can detect and invalidate stolen tokens without a server-side session store.
-
Signing a token is not encrypting it. The difference matters.
Signing proves integrity and authenticity. Encryption provides confidentiality. Confusing the two leads to real security vulnerabilities in authentication systems.
-
A JWT is three base64 strings. Here's what's actually inside yours.
Decode a real JWT and understand what each of the three parts contains, why they're base64url encoded, and what you can and cannot trust.
-
Swagger UI without polluting your app: CDN-hosted docs in 10 lines.
Swagger UI can be served from a CDN with a single HTML file and no npm packages. Here's how to wire it to your OpenAPI spec without adding dependencies to your app.
-
OpenAPI spec: the minimum you need to write and what it unlocks.
OpenAPI specs enable auto-generated docs, client SDK generation, and request validation. Here's the minimum viable spec and what you get from writing it.
-
API error responses: the format that makes frontend error handling not miserable.
Inconsistent error responses force frontend developers to write brittle parsing code. A predictable error format makes client-side error handling straightforward.
-
Idempotency: why your PUT endpoint should be safe to call twice.
Idempotency is a property of HTTP methods that makes distributed systems more reliable. Here's what it means, which methods must have it, and how to implement it for POST.
-
Offset pagination breaks at scale. Here's how cursor pagination fixes it.
OFFSET-based pagination is easy to implement but produces inconsistent results as data changes. Cursor pagination is stable, performant, and the right default for most APIs.
-
API versioning: 3 strategies, 1 that doesn't create maintenance debt.
API versioning is how you change your API without breaking existing clients. Here are three common strategies, their trade-offs, and the one that scales without accumulating maintenance burden.
-
HTTP 200 is not always success. The status codes that make a difference.
Using 200 for everything is technically wrong and makes error handling harder for API consumers. Here are the status codes worth using and what each one communicates.
-
URL design: naming conventions that prevent bikeshedding on every PR.
URL structure debates slow down teams. Establishing consistent naming conventions up front eliminates the recurring arguments and makes your API predictable.
-
REST constraints: the 3 that actually change how you write code.
REST has six architectural constraints. Three of them directly shape how you design endpoints and handle state. Here's what they mean in practice.
-
Express app structure: the folders that keep a growing codebase navigable.
A flat Express project works until it doesn't. Here's a folder structure that separates concerns, scales with the project, and stays readable for new contributors.
-
Rate limiting without a third-party service: the pattern that holds up.
You don't need Redis or an external service to rate limit a Node.js API. Here's the in-process pattern that works for single-instance apps and the point where you need to upgrade.
-
Helmet adds 11 security headers in one line. Here's what each does.
Helmet is a collection of Express middleware that sets HTTP security headers. Here's what each header actually does and why you want it enabled.
-
CORS: the config that works and the one that silently breaks everything.
CORS errors happen in the browser, not the server, which makes them confusing to debug. Here's how CORS actually works and the Express config that gets it right.
-
Validate the request before it reaches your handler.
Putting validation logic inside route handlers pollutes business logic and lets bad input reach your database. Here's how to intercept it earlier with middleware.
-
Router-level vs app-level middleware: picking the wrong one breaks auth.
App-level and router-level middleware behave differently in ways that aren't obvious until auth stops working. Here's what each one does and when to use it.
-
Express error handling has one rule everyone gets wrong: the 4-arg signature.
Error handling in Express works differently from regular middleware. One wrong function signature and your errors silently pass through. Here's how it actually works.
-
The middleware chain is everything in Express. Here's how requests move.
Understanding how a request flows through Express middleware is the foundation of every feature you'll build. Here's how the chain actually works.
-
Express routing patterns that don't turn into spaghetti at scale.
A flat routes file works for 5 endpoints. Here are the patterns that keep an Express app navigable at 50 or 500 endpoints.
-
Memory leaks in Node look like slowdowns. Here's how to find them.
Memory leaks in Node.js cause gradual performance degradation. Here's how to identify common patterns and use heap snapshots to find them.
-
Debugging Node: the --inspect flag and when console.log is the last resort.
Node.js has a full debugger accessible from Chrome DevTools. --inspect gives you breakpoints, call stacks, and memory inspection.
-
Graceful shutdown: the signal handler every production Node app needs.
Without a shutdown handler, your Node process drops active connections when it stops. Here's how to finish in-flight requests before exiting.
-
Worker threads: CPU-bound work that doesn't block your server.
Node.js is single-threaded, but worker threads let you run CPU-intensive code in parallel without blocking the event loop.
-
Never hardcode a secret. Here's how Node env variables actually work.
Environment variables are the standard way to configure Node.js applications without hardcoding secrets. Here's how they work end to end.
-
path.join vs path.resolve: not interchangeable. The difference matters.
path.join concatenates path segments. path.resolve builds an absolute path. Using the wrong one produces different results depending on environment.
-
child_process: running shell commands from Node without shooting yourself.
Node's child_process module lets you run shell commands, but the API choices matter. Here's how to use it safely and correctly.
-
Streams in Node: the only way to handle files bigger than RAM.
Node.js streams process data incrementally. They are the right tool for large files, network data, and any situation where you can't load everything into memory.
-
Node event emitters: the pattern under half of the ecosystem.
EventEmitter is the backbone of Node.js streams, HTTP servers, and many popular packages. Understanding it makes the rest of Node.js click.
-
require() vs import: they look the same and work completely differently.
CommonJS require() and ES module import are not interchangeable. Understanding the difference explains module compatibility issues and bundler behavior.
-
The TypeScript pattern that made me stop using any everywhere.
unknown is the type-safe alternative to any. Combined with type guards, it handles dynamic data without sacrificing type safety.
-
Template literal types: string manipulation before your code runs.
Template literal types let TypeScript reason about string values at the type level. They enable precise typing of string unions and event names.
-
Mapped types: building a new type from an existing one, automatically.
Mapped types iterate over the keys of a type and transform them. They are the foundation of most TypeScript utility types.
-
The assertNever trick: catching missing switch cases at compile time.
assertNever is a one-function pattern that turns incomplete switch statements from runtime bugs into compile errors.
-
Typed Express: req, res, next with types that don't lie.
Express's default TypeScript types are loose. Here's how to get accurate types on req.body, req.params, res.json, and custom middleware.
-
DTOs: the pattern that keeps API input from contaminating business logic.
Data Transfer Objects create a boundary between what comes in over the wire and what your application actually works with.
-
TypeScript strict mode turns on 7 flags. Here's what each one catches.
strict: true in tsconfig is not one setting. It enables 7 independent checks. Understanding each one helps you know what you're getting.
-
interface vs type alias: the actual differences after years of use either.
interface and type alias overlap almost completely. The real differences are declaration merging and expressiveness for complex types.
-
Type narrowing: how the compiler gets smarter as your code runs.
TypeScript tracks which types are possible at each point in your code. Understanding narrowing lets you write more precise types and fewer casts.
-
Branded types: making string IDs impossible to mix up.
Branded types add nominal typing to TypeScript's structural system, preventing assignment between semantically different types with the same structure.
-
Pick, Omit, Partial: 3 utility types that cover 80% of cases.
TypeScript's built-in utility types let you transform existing types without rewriting them. Pick, Omit, and Partial cover most daily needs.
-
TypeScript's discriminated union is the closest thing it has to a superpower.
Discriminated unions model state that cannot be invalid. They eliminate impossible states by making them unrepresentable.
-
Generics without the abstract nonsense. One real example.
Generics let functions and types work with any type while preserving type information. One practical example makes them concrete.
-
Union types sound simple. They eliminate entire bug categories.
Union types in TypeScript are more than a list of allowed values. They enforce handling every case at compile time.
-
Immutability in JavaScript without a library.
You don't need Immutable.js or Immer for most immutability needs. Native JavaScript gives you the tools.
-
Spread syntax does two different things depending on where you put it.
The ... syntax is used in two fundamentally different ways in JavaScript. Knowing the difference makes destructuring and function signatures clearer.
-
Default exports break refactoring. Named exports don't.
Default exports let consumers name imports anything they want. This creates invisible coupling that makes renaming and codebase searches unreliable.
-
Map and Set exist for a reason. Here's when arrays are the wrong choice.
Arrays are the default collection in JavaScript, but Map and Set are more correct for specific use cases. Here's when and why.
-
Why setTimeout(fn, 0) doesn't run immediately.
setTimeout with a zero delay is not immediate. Understanding why requires understanding the event loop.
-
Closures are not magic. One example that makes them click forever.
Closures are the mechanism behind half of JavaScript's patterns. One concrete example explains them permanently.
-
Optional chaining saved me from 40 null checks. Here's how.
Optional chaining (?.) is not just shorter code. It changes how you navigate uncertain data structures.
-
Destructuring looks simple. It has one trap that breaks real code.
Destructuring is convenient, but renaming, defaults, and nested patterns have edge cases that catch everyone at some point.
-
You're using async/await without knowing what it hides.
async/await is syntactic sugar over Promises. Understanding what it compiles to explains every confusing behavior.
-
Stop writing for loops. These 7 array methods do it better.
For loops are verbose, error-prone, and harder to read. Modern array methods express intent and reduce bugs.