PRODUCT BUILDERJakub KulhanJK
← Skills library

Backend development

Building the server-side logic, data processing, and integrations that power the product. Covers the patterns and principles that make backend code maintainable, not just working.

DeliveryEngineerProduct Builder
Backend development
ACTIVITIES

What you need to do

The practical activities and techniques you need to master – what to practice, what to get good at, and what to keep doing.

Implementing request-response

Building the synchronous communication layer of your backend – HTTP endpoints that receive requests and return responses. Structure route handlers as thin wrappers: validate input, call a service, return a response. Never put business logic in a route handler. Use middleware for cross-cutting concerns: authentication (verify the user), logging (record what happened), error handling (catch exceptions and return consistent error responses), rate limiting (protect the endpoint). Stack middleware in the right order: logging first, authentication next, validation last. Build your error handling middleware early – it catches unhandled exceptions and returns a consistent shape instead of a stack trace.

Implementing consumers

Building the asynchronous communication layer – workers that process messages from queues, event streams, or pub/sub systems. Not everything belongs in the request-response cycle. Email sending, image processing, webhook delivery – these should happen in background consumers. BullMQ with Redis is the standard for Node.js: define a job, add it to a queue, process it in a worker. Jobs can retry on failure, run on a schedule (cron), and report progress. For simpler needs, Upstash QStash provides serverless job queues. The rule: if it takes more than 200ms or can fail independently, put it in a queue.

Implementing batch jobs

Building scheduled or one-off processes that operate on large datasets – report generation, data migrations, nightly syncs, cleanup tasks. Batch jobs differ from consumers in that they're typically triggered by time (cron schedules) rather than events. Design batch jobs to be idempotent (safe to re-run), resumable (can pick up where they left off after failure), and observable (log progress so you know where they are). Set timeouts and deadlines. Monitor execution time – a batch job that gradually slows down is a leading indicator of data growth problems.

Data layer

Building the interface between your application and its data stores – databases, caches, external APIs. Use an ORM (Prisma, Drizzle) for type-safe database access with parameterized queries. Wrap each external integration in a dedicated client module: one file that handles authentication, request formatting, error mapping, and retries. Never scatter fetch() calls with API keys across your codebase. Validate data at the boundary: use schema validation (Zod) for incoming data and type-safe query clients for outgoing queries. The data layer is the seam that makes your application testable – services that receive their data dependencies can be tested with mocks or fakes.

Configuration and secrets

Environment variables are the standard for configuration – database URLs, API keys, feature flags. Never hardcode secrets in source code. Use a .env file for local development (add it to .gitignore immediately), and your deployment platform's secrets management for production. Validate environment variables at startup with Zod or a similar library – fail fast with a clear error message if a required variable is missing. This prevents the nightmare of discovering a missing API key at 3am when the payment webhook fires.

KEY FRAMEWORKS

Frameworks to know

The mental models and structured approaches that guide how you think about this skill – not rules to follow, but lenses to see through.

MVC (Model-View-Controller)

The foundational architectural pattern for separating concerns in web applications. Model handles data and business logic, View handles presentation, Controller handles input and orchestrates between Model and View. In modern backends, this translates to: Models are your domain entities and database schemas, Views are your API response shapes (JSON serialization), Controllers are your route handlers that receive requests and delegate to services. MVC's lasting contribution isn't the specific three-layer split – it's the principle that presentation, business logic, and data access should be independent, swappable, and testable in isolation.

Dependency injection

Instead of importing concrete implementations directly, inject dependencies through constructors or factory functions. A service that needs a database client receives it as a parameter, not as a hardcoded import. This makes testing trivial: pass a mock database client in tests, the real one in production. You don't need a DI framework – a simple factory function that wires up dependencies at startup achieves the same result. The principle matters more than the tool: code that receives its dependencies is easier to test, easier to refactor, and easier to understand than code that reaches out and grabs them.

THE PRODUCT BUILDER WAY

Good vs. bad

Common mistakes paired with what experienced practitioners do instead – and why the product builder approach changes the game.

Business logic in route handlers
A 200-line API route that validates input, queries the database, applies business rules, sends emails, and formats the response. When you need the same logic in a webhook handler, you copy-paste and pray both copies stay in sync.
Thin handlers, rich services
Route handler validates input and calls orderService.create(). The service contains the business logic, reusable from any entry point – API routes, webhooks, background jobs, CLI scripts.
Raw SQL strings with concatenation
query(`SELECT * FROM users WHERE email = '${email}'`) – one apostrophe in an email address and you have a SQL injection vulnerability. No type safety, no IDE autocomplete, no compile-time checks.
ORM with parameterized queries
prisma.user.findUnique({ where: { email } }) – injection-safe by default, type-checked at compile time, IDE autocomplete for every field. The query your code expresses is the query that runs.
Hardcoded API keys in source code
const STRIPE_KEY = 'sk_live_abc123' committed to the repository. Every developer, every CI runner, and anyone with repo access has your production payment credentials.
Environment variables validated at startup
process.env.STRIPE_KEY loaded from platform secrets, validated with Zod at server startup. Missing or malformed keys fail immediately with a clear error message, not at midnight when the webhook fires.
Synchronous work blocking the event loop
Generating a PDF, resizing an image, or running a complex calculation inside a request handler. Every other request waits in line behind it. Response times spike from 50ms to 5 seconds.
CPU-heavy work in background jobs
Add a job to the BullMQ queue, return 202 Accepted immediately. A worker processes the job asynchronously – retries on failure, reports progress, and doesn't block a single API request.
No error handling middleware
Unhandled exceptions crash the process or leak stack traces to the client. Each route handler has its own try/catch with slightly different error formats. Frontend developers guess which error shape they'll get.
Centralized error handler with consistent shapes
One error middleware catches everything, maps exceptions to HTTP status codes, and returns a consistent { code, message, details } shape. Stack traces go to logs, not to clients.
Scattered fetch() calls with inline config
Every file that calls the Stripe API has its own fetch() with the API key, its own error handling, and its own retry logic. When Stripe changes an endpoint, you grep and hope.
Dedicated client modules per integration
One stripe.ts file wraps the API: authentication, error mapping, retries, and timeout – all in one place. The rest of the codebase calls stripeClient.createPayment(), not fetch().

Backend development is where product builders spend most of their coding time – and where AI tools give the biggest productivity boost. Claude Code or Cursor with a well-typed TypeScript codebase can scaffold an entire API endpoint in minutes: route handler, service, database query, validation, error handling. The AI understands the types and generates code that fits your existing patterns. The product builder's job shifts from writing boilerplate to reviewing and refining what the AI produces.

I've settled on a simple stack for most backend work: Next.js API routes for the application, Prisma for database access, Zod for validation, and BullMQ for background jobs. It's boring, well-documented, and every TypeScript developer can read it. The temptation to adopt NestJS or Fastify or tRPC is real – and sometimes justified – but for most products, Next.js API routes are enough. Add complexity when you feel pain, not when you anticipate it.

The layered architecture (handlers → services → repositories) isn't about architecture astronautics – it's about testability. When your business logic lives in a service function that takes typed inputs and returns typed outputs, testing it is trivial. When the same logic is tangled inside a route handler that depends on HTTP request objects, testing it requires mocking the entire web framework. I've seen teams abandon testing entirely because their code was too coupled to test. Layers prevent that.

Supabase changed how I think about backend development for new products. Authentication, file storage, real-time subscriptions, and a PostgreSQL database – all managed, all with generous free tiers. For the first version of any product, the question isn't 'should I build auth from scratch?' It's 'can I ship by Thursday?' Supabase says yes. You can always migrate to custom infrastructure later, when you've proven the product has users who care.

The single most impactful backend practice I've adopted is validating environment variables at startup with Zod. It takes 10 minutes to set up and it prevents every 'why is this failing in production?' incident that's caused by a missing or misconfigured environment variable. Server starts, Zod validates every required env var, and if anything is missing, the server exits immediately with a human-readable error. No more discovering a missing Stripe key at 2am.

SOURCES

Go deeper

Books to deepen your understanding beyond what fits on this page.

Node.js Design Patterns
Node.js Design Patterns
Mario Casciaro & Luciano Mammino · Book

The definitive guide to Node.js patterns – event loop, streams, async patterns, dependency injection, middleware, and module design. Now in its fourth edition with TypeScript coverage and modern Node.js APIs.

Designing Data-Intensive Applications
Designing Data-Intensive Applications
Martin Kleppmann · Book

The backend developer's bible for understanding how data moves through systems – storage engines, replication, stream processing, and the trade-offs behind every architectural decision.

Dependency Injection: Principles, Practices, and Patterns
Dependency Injection: Principles, Practices, and Patterns
Steven van Deursen & Mark Seemann · Book

The canonical reference on DI – not just how containers work, but why composition root, lifetime management, and constructor injection make code testable and maintainable. Language-agnostic principles with C# examples.

Enterprise Integration Patterns
Enterprise Integration Patterns
Gregor Hohpe & Bobby Woolf · Book

The pattern language for asynchronous messaging – queues, topics, channels, routing, and transformation. The book behind how RabbitMQ, Kafka, and SQS are actually used in production systems.

Redis in Action
Redis in Action
Josiah L. Carlson · Book

Foreword by Salvatore Sanfilippo (Redis creator). Covers Redis as cache, queue, pub/sub, leaderboard, session store, and rate limiter – the use cases that make Redis a Swiss Army knife in modern backends.

Relevant Search
Relevant Search
Doug Turnbull & John Berryman · Book

How to build search that actually returns what users want – with applications for Solr and Elasticsearch. Covers analyzers, scoring, signals modeling, and the practical work of making search relevant.