Skip to content

Server-Side Tools: The Framework Is the Easy Part

Most backend tooling advice is a list of web frameworks, which matters least. The data layer, the boundaries, and observability determine whether it works.

Share
Abstract visualisation of a backend toolkit as connected components around a central core

Ask what tools a backend developer needs, and you get a list of web frameworks. Express, Django, Rails, Laravel, Spring Boot, and so on, down the list.

It is a strange answer when you consider it. Framework choice is one of the least consequential decisions on a typical backend project. Any of them will route a request, run middleware, and return a response competently, and a team productive in one would be productive in another within a fortnight. Meanwhile, the decisions that actually determine whether a system holds up, how the data is modelled, where the boundaries sit, what happens when something slow needs to happen out of band, and whether you can tell what went wrong at three in the morning, are barely mentioned.

So this covers the framework question briefly, because it does have to be answered, and then spends the rest of its time on the parts that matter more.

Picking a runtime and framework

The choice is mostly determined by things other than the framework.

What your team already knows dominates everything else. A team fluent in Python will ship better software in Django than in an objectively superior framework they are learning on the job.

What the ecosystem offers for your problem: Machine learning work pulls towards Python. Enterprise integration and long-lived systems pull towards Java or .NET. Anything sharing types with a TypeScript front end pulls towards Node.

Who you can hire, which is a real constraint for a company and irrelevant for a side project.

The concurrency model, which is the one genuinely technical differentiator. Node's event loop is excellent for high-concurrency I/O-bound work and poor for CPU-bound work, which is why heavy computation in a Node service blocks everything. Go's goroutines and the JVM's threading handle CPU-bound and mixed workloads more gracefully.

With that said, here is the landscape, grouped by language rather than ranked, since ranking them is a category error.

JavaScript and TypeScript: Express remains the default and most widely understood, and after an extremely long gap, it has a modern major version, though much of what is written about it still targets version 4. Fastify is the sensible choice when throughput matters and includes built-in schema validation. Hono is the one to look at for edge and serverless deployment, being small and portable across runtimes. NestJS suits larger teams who want opinionated structure and dependency injection, and feels familiar to anyone from a Spring or Angular background. On runtimes, Node is the safe default, Bun is meaningfully faster and worth considering for new projects, and Deno's appeal is its security model and built-in tooling.

Python: Django when you want the batteries included: admin interface, ORM, auth and migrations all present from the start, which is a genuine advantage for anything content- or workflow-heavy. FastAPI has become the default for building APIs because it derives validation and OpenAPI documentation from type hints, removing an entire category of drift between code and contract. Flask remains fine for small services, though FastAPI has taken most of the ground it used to occupy. Worth noting that Django's pattern is usually described as model-template-view rather than MVC, which trips people up when they arrive expecting the names to line up.

PHP: Laravel, which is genuinely pleasant and has an unusually complete ecosystem for queues, scheduling, and admin tooling. Symfony, where you want something more component-based and less opinionated.

Ruby: Rails, which remains one of the fastest ways to get from nothing to a working product, and whose convention-over-configuration approach is still the benchmark others are measured against.

Java and Kotlin: Spring Boot for anything enterprise, with the deepest ecosystem of the lot and correspondingly heavy defaults. Quarkus and Micronaut are the lighter alternatives when startup time and memory matter, particularly in containers.

.NET: ASP.NET Core, which is worth naming precisely, because "ASP.NET" without "Core" refers to the older Windows-only framework. Core is cross-platform, fast and genuinely good, and the confusion between the two is the single most common thing people get wrong about the .NET ecosystem.

Go: Often the standard library plus a light router such as chi, which is a legitimate and popular choice rather than an austere one. Gin and Echo, where you want more. Go's appeal for services is deployment simplicity: one static binary, no runtime to install.

Rust: Axum, where performance and correctness matter more than development speed. Worth being honest that the productivity cost is real, and it is the right call for a narrower set of problems than its enthusiasm suggests.

That is the whole framework question. Now the parts that decide whether the system is any good.

The data layer, which is where the real decisions are

Schema mistakes outlive every other kind. Frameworks get replaced in a weekend; a data model that made the common query expensive will still be causing problems in five years.

The database: PostgreSQL is the default and should be your reason-to-deviate baseline, not a considered choice. It handles relational work, JSON documents, full-text search, geospatial data, and, increasingly, vector search, meaning one system covers what used to require three. MySQL and MariaDB remain perfectly good, particularly where you have existing expertise. SQLite deserves more credit than it gets: for single-server applications and read-heavy workloads it is fast, operationally trivial and increasingly viable in production. Reach for a document database when your data genuinely is document-shaped, and be honest: most applications that reach for one are storing relational data awkwardly.

The ORM question, which generates more argument than it deserves. An ORM removes boilerplate and makes simple things quick. It also hides what the database is doing, which is how N+1 query problems get shipped. The workable position: use one, and know enough SQL to read the queries it generates and to drop into raw SQL when the ORM fights you. In practice that means SQLAlchemy for Python, ActiveRecord for Rails, Eloquent for Laravel, Entity Framework Core for .NET, and for TypeScript either Prisma for its developer experience or Drizzle if you want something closer to SQL.

Migrations, always: Schema changes belong in version control, applied by a repeatable tool, tested against a copy of production data. Every framework above has one. Using it is not optional, and manual schema changes on a live database are how teams end up unable to recreate their own environment.

The underlying skill here is SQL itself, not any of the tools. Understanding indexes, query plans, and transaction isolation transfers across every database and outlasts every ORM.

Caching, and a licence change worth knowing about

Caching is the highest-leverage performance work available on most backends, and the default tool changed recently in a way much of the published advice hasn't caught up with.

Redis moved from the permissive BSD licence to a dual source-available model in March 2024, which prompted the Linux Foundation, with AWS, Google and Oracle behind it, to fork the last BSD release into Valkey. Redis then added AGPLv3 as an option in Redis 8 in May 2025, returning to an OSI-approved licence, but by then Valkey had its own governance and momentum. Both are healthy and actively developed, and Valkey remains wire-protocol compatible, so switching is generally undramatic.

Practical guidance: if you want a permissive BSD licence with no copyleft obligations, use Valkey, which most Linux distributions now ship by default. If you are already on Redis and the AGPL terms are acceptable, staying is fine. If you are offering a managed service built on it, read the licence properly rather than taking anyone's summary, including this one.

Beyond the tool, the key thing to internalise is that caching is easy to add and hard to invalidate. Cache at the layer where the cost actually is, set expiry deliberately rather than by default, and remember that a cache serving stale data is a correctness bug rather than a performance optimisation.

Background work

The moment you have anything that takes longer than a request should, sending email, generating a report, processing an upload, calling a slow third party, you need work happening outside the request cycle. Doing it inline is the most common cause of a backend that feels slow for no visible reason.

The ecosystem answers: Celery for Python, Sidekiq for Ruby, Laravel's built-in queues for PHP, BullMQ for Node, and Hangfire or the built-in hosted services for .NET. Underneath, a queue, usually Redis or Valkey for simpler needs, RabbitMQ or Kafka when you need durability, ordering and replay.

The design points that matter more than the tool: make jobs idempotent, because they will be retried; give them a retry policy with backoff rather than infinite immediate retries; and monitor the queue depth, since a growing backlog is often the first visible sign that something downstream has failed.

The API contract

This is the boundary where most integration pain lives, and it is worth treating as a first-class artefact rather than a byproduct.

Generate an OpenAPI specification from your code rather than maintaining it separately, since a hand-written specification drifts from reality within weeks. FastAPI does this natively, and every other framework has a mature way to do it.

For TypeScript teams sharing a codebase across client and server, tRPC removes the boundary entirely by making calls type-checked end to end, eliminating a class of bugs at the cost of coupling the two sides.

GraphQL solves a real problem where many different clients need different shapes of the same data, and introduces genuine complexity in caching, authorisation and query cost control. It is a good answer to that specific problem, but an expensive one in the general case.

Whatever you choose, decide your versioning strategy before the first external consumer arrives, not after.

Knowing what is happening

The tooling that separates a system you can operate from one you can only hope about.

Structured logging, meaning JSON with consistent fields rather than interpolated strings, because logs you cannot query are logs you will not read. Pino for Node, structlog for Python, and the built-in structured logging in Go and .NET.

OpenTelemetry as the instrumentation standard, which matters because it is vendor-neutral: instrument once, and change your backend without reinstrumenting. That was not true of the previous generation of agents.

Error tracking, with Sentry as the obvious choice, giving you grouped exceptions with stack traces and context rather than a log line somebody has to notice.

Metrics and dashboards, with Prometheus and Grafana as the standard self-hosted pairing, plus plenty of hosted alternatives if you would rather not run them.

Aim to answer, for any given slow or failed request, where the time went and what the inputs were, without adding new logging and waiting for it to happen again.

The surrounding pieces

Briefly, because they are less contentious.

Containers and local development: Docker Compose remains the simplest way to give every developer the same database, cache and queue as production, and to make onboarding a single command.

Testing: The unit, integration and end-to-end split applies on the server too, and the highest-value tests are usually integration tests against a real database rather than a mocked one, since the mocking is where the divergence hides. The general principles carry over from what I wrote about testing tools, even though the specific tools differ.

Continuous integration, which by now is table stakes: run tests, run migrations against a scratch database, build the container, block the merge if any of it fails.

Secrets management, which means not in the repository and not in an environment file committed by accident. Whatever your platform provides is usually sufficient and always better than the alternative.

What to actually learn

If you are deciding where to spend limited time, the ordering that pays back best is roughly the inverse of how these topics are usually taught.

SQL and data modelling first, because they transfer everywhere and the mistakes are the most expensive. Then HTTP itself, properly: status codes, caching headers, content negotiation, what a connection actually does. Then one framework, well. Then the operational layer: logging, metrics, and how to read a trace.

Notice that only one item on that list is a framework, and it is the third one.

The reason is the same one that makes full-stack work valuable, which I covered when I wrote about what that label actually means: the interesting problems live at the boundaries between components rather than inside them. A framework is a well-documented box. The joins between the boxes are where the systems fail, and no tool list will teach you those.