Givepo
Blog

Backend Architecture Explained: REST APIs, Microservices and Scalable Databases

How to design REST APIs developers enjoy using, when microservices are worth their cost, how to model a database that holds up under load, and the patterns that keep backend systems fast and reliable.

Givepo Team
6 min read
Illustration of a client sending a GET request to an API gateway that connects to three services and a databaseBackend & APIs

Every app that feels fast and dependable has a well-designed backend behind it. The backend owns the business rules, guards the data and answers thousands of requests a second without anyone noticing. Getting its architecture right early saves months of rework later, and getting it wrong shows up as outages, slow pages and features that take far longer than they should.

This article walks through the decisions that matter most: how to design APIs, how to structure services, how to model data, and how to keep it all running as traffic grows.

Designing REST APIs people enjoy using

REST models your system as resources (nouns) that clients manipulate with standard HTTP methods (verbs). Consistency is the whole point: once a developer has learned one endpoint, they should be able to guess the rest.

  • GET /v1/orders lists orders, with filtering and pagination through query parameters.
  • GET /v1/orders/{id} returns one order.
  • POST /v1/orders creates an order and responds with 201 Created and a Location header.
  • PATCH /v1/orders/{id} updates part of an order; PUT replaces it entirely.
  • DELETE /v1/orders/{id} removes it and responds with 204 No Content.

Status codes are part of your contract, so use them precisely:

  • 200 OK, 201 Created and 204 No Content for success.
  • 400 Bad Request for malformed requests and 422 Unprocessable Content for well-formed requests that fail validation.
  • 401 Unauthorized when the caller is not authenticated, and 403 Forbidden when they are authenticated but not allowed.
  • 404 Not Found, 409 Conflict for state conflicts such as duplicate records, and 429 Too Many Requests when rate limited.
  • 5xx only for failures on the server side, never for bad input.

Here is an endpoint in TypeScript with Express that validates input at the boundary and returns the right codes:

TypeScript
import express from 'express'
import { z } from 'zod'
import { orders } from './orders'

const app = express()
app.use(express.json())

const CreateOrder = z.object({
  customerId: z.string().uuid(),
  items: z.array(z.object({ sku: z.string().min(1), quantity: z.number().int().positive() })).min(1),
})

app.post('/v1/orders', async (req, res) => {
  const parsed = CreateOrder.safeParse(req.body)
  if (!parsed.success) {
    return res.status(422).json({
      type: 'https://api.example.com/problems/validation',
      title: 'The request body is invalid',
      status: 422,
      errors: parsed.error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message })),
    })
  }

  const order = await orders.create(parsed.data)
  res.status(201).location(`/v1/orders/${order.id}`).json(order)
})

Consistent errors, pagination and versioning

Return every error in the same shape so clients can handle them generically. The Problem Details format (RFC 9457) is a widely supported standard:

JSON
{
  "type": "https://api.example.com/problems/out-of-stock",
  "title": "Item is out of stock",
  "status": 409,
  "detail": "SKU TS-1042 has 0 units available; 3 were requested.",
  "instance": "/v1/orders"
}
  • Pagination: prefer cursor-based pagination (?after=ord_123&limit=50) for large or fast-changing collections. Offset pagination skips or repeats items when rows are inserted while a client is paging.
  • Versioning: put a major version in the path (/v1/) and only add fields within a version. Removing or renaming a field is a breaking change.
  • Idempotency: let clients send an Idempotency-Key header on POST requests, a pattern popularized by payment APIs, so a retried request never creates a second order or charge.

Monolith or microservices?

A monolith is one deployable application. Microservices split the system into small services that each own their data and are deployed independently. Microservices are often presented as the modern default, but they trade code complexity for operational complexity:

  • What you gain: independent deployments, scaling hot spots separately, and teams that can work without stepping on each other.
  • What you pay: network calls that can fail or slow down, data spread across databases with no shared transactions, distributed tracing and debugging, and much more infrastructure to run.

For most products the right starting point is a modular monolith: one deployable, but with strict internal boundaries (orders, billing, catalog) that only talk through defined interfaces. If one module later needs to scale or ship independently, its boundary is already clean enough to extract it as a service.

Split a system along business boundaries when a team or a scaling need demands it, not because the architecture diagram looks better.

When services do need to talk, use synchronous calls (REST or gRPC) when the caller needs an answer right away, and asynchronous events through a message broker when other services only need to react: "order placed", "payment received". To publish events reliably, write them to an outbox table in the same database transaction as the business change and let a separate process relay them to the broker. Otherwise a crash between the two steps leaves your data and your events out of sync.

Database design that scales

Most performance problems are database problems. A few principles go a long way:

  • Model the domain first, then the queries. Start normalized, so each fact lives in one place, and denormalize deliberately where a measured query needs it.
  • Let the database enforce the rules: NOT NULL, foreign keys, UNIQUE and CHECK constraints catch bugs that application code misses.
  • Store money as integers (cents) or a decimal type, never as floating-point numbers.
  • Index for your real queries, and check them with EXPLAIN. Every index speeds up reads but slows down writes.
  • Change the schema through versioned migrations that run automatically, never by hand in production.
SQL
CREATE TABLE orders (
  id          UUID PRIMARY KEY,
  customer_id UUID NOT NULL REFERENCES customers (id),
  status      TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Serves "latest orders for a customer" without scanning the whole table
CREATE INDEX orders_customer_created_idx ON orders (customer_id, created_at DESC);

Watch out for the N+1 query problem: loading 50 orders and then running one extra query per order to fetch its customer. Fetch related data in one query with a join or a batched WHERE id IN (…) instead. Use a connection pool as well, since opening a database connection per request is slow and exhausts the server quickly.

Scaling patterns that work

  1. Keep application servers stateless (sessions in a database or cache, files in object storage) so you can run as many copies as you need behind a load balancer.
  2. Cache expensive reads with a short time-to-live, and decide up front how each cache entry is invalidated when the data changes.
  3. Move slow work to background queues: emails, reports, image processing and calls to third parties. The API responds immediately and workers catch up.
  4. Add read replicas when reads dominate, and send reporting queries there rather than to the primary.
  5. Rate-limit public endpoints to protect the system from abuse and runaway clients.
  6. Partition or shard data last, only when a single well-tuned database truly cannot keep up.

Reliability and observability

Distributed systems fail in partial, confusing ways, so design for it. Set a timeout on every outbound call. Retry only idempotent operations, with exponential backoff and random jitter so clients don’t retry in lockstep. Use a circuit breaker to stop calling a dependency that is clearly down.

Then make the system observable: structured logs with a request ID, metrics for latency, traffic, errors and saturation, and distributed traces, for example with OpenTelemetry, that show where time goes across services. Alert on symptoms users feel, such as error rate and latency, rather than on every CPU spike.

Key takeaways

  • Design APIs around resources, precise status codes and one consistent error format.
  • Start with a modular monolith; extract services along business boundaries when there is a real need.
  • Let the database enforce integrity, index for real queries, and avoid N+1 queries.
  • Scale with stateless servers, caching and queues, and build in timeouts, retries and observability from the start.

Conclusion

Scalable backends are not built from exotic technology. They come from boring, consistent decisions applied everywhere: clear API contracts, clean boundaries between parts of the system, a well-modelled database and the operational habits that make problems visible early. Get those right and adding capacity becomes a configuration change rather than a rewrite.

Further reading

  • #Architecture
  • #Databases
  • #Microservices
  • #REST API
  • #TypeScript
Written by
Givepo Team

Givepo is an IT agency building websites, mobile apps and custom software that move your business forward.

Share this article

Free 30-minute consultation

Talk through your idea, scope and budget with our team.

Book a consultation

More articles

Blog
Software Engineering
·7 min read

Clean Architecture and SOLID Principles: Writing Software That Stays Maintainable

Why codebases slow down as they grow, and how clean architecture, the dependency rule and the SOLID principles keep business logic testable and easy to change, with a complete TypeScript example.

View details
Web Development
·6 min read

Modern Web Development with Next.js, React and TypeScript: Best Practices

Server Components, rendering strategies, type-safe data and Core Web Vitals. A practical guide to building fast, maintainable web applications with the stack most teams choose today.

View details
AI & Machine Learning
·7 min read

AI and Machine Learning in Practice: Where They Deliver Real Business Value

Cut through the hype. Learn how machine learning and generative AI actually work, where they create measurable value today, and how to run a first AI project that ships instead of stalling as a demo.

View details