Backend Service Capstone: Build, Ship, and Operate One API

LESSON

Backend Development Foundations

016 25 min beginner CAPSTONE

Backend Service Capstone: Build, Ship, and Operate One API

By the end of this lesson, you will be able to...

  • Review one backend request from API contract to durable state and production evidence.

  • Identify gaps that make a service look complete but unsafe to operate.

  • Use a readiness rubric to decide whether a small API is ready to ship.

Idea in one sentence: A backend is ready to ship when one important request can be explained from client promise to database state, tests, release path, and operational signals.

Core Insight

Imagine you are building the first orders API for a small shop.

The feature list looks ordinary:

create an order
read an order
list my orders
let admins manage products
store data in a relational database
run tests
build a container
deploy the service
emit logs and metrics

A beginner can turn that list into a set of disconnected tasks. One route returns 201. One table stores orders. One test passes. One image builds. One dashboard exists.

That can still produce a backend nobody trusts.

The capstone question is not:

Did each checklist item appear somewhere?

The better question is:

Can one important request be explained end to end?

Use POST /orders.

A client sends JSON. The backend authenticates the caller, checks authorization, validates input, reads products, calculates the total, writes durable rows, prevents duplicate retries, returns a clear response, proves the behavior with tests, ships a repeatable artifact, and leaves evidence that can explain success or failure in production.

The mental model is:

one request -> many boundaries -> one chain of evidence

Each boundary asks a different question:

contract: what did we promise the client?
identity: who is making the request?
validation: is the input acceptable?
database: what became durable?
tests: what evidence do we have before release?
deployment: what artifact is running?
observability: what happened after users touched it?

If those answers agree, the service is small but coherent. If they disagree, adding more endpoints usually makes the service harder to trust.

The Scenario

Build a small API with five routes:

POST /orders
GET /orders/:id
GET /orders
POST /admin/products
PATCH /admin/products/:id

The service owns:

orders
order items
product catalog

It does not own:

payment processing
email delivery
warehouse fulfillment

Those can be mocked, queued, or integrated later. Ownership matters because a backend that owns everything soon has no clear boundary.

A minimal relational model might be:

users(id, email, role)
products(id, sku, name, price_cents, active)
orders(id, user_id, status, total_cents, idempotency_key, created_at)
order_items(id, order_id, product_id, quantity, unit_price_cents)

The capstone focuses on one request:

POST /orders

Request:

Authorization: Bearer <token>
Idempotency-Key: <client-generated-key>

{
  "items": [
    { "product_id": "p1", "quantity": 2 }
  ]
}

Success:

201 Created

{
  "order_id": "ord_123",
  "status": "created",
  "total_cents": 5000
}

Expected client errors:

400 malformed JSON or invalid quantity
401 missing or invalid authentication
403 authenticated user is not allowed
404 product does not exist or is not visible
409 same idempotency key with different input

This contract is not documentation decoration. It is the shared promise that shapes handler code, database constraints, tests, release checks, and observability.

Plain meaning:

A service boundary says what this backend owns, promises, rejects, and records.

In this scenario:

The orders API owns order creation and product catalog state, but not payment settlement or email delivery.

Technical name:

This is the backend service boundary.

The Naive Completion

The tempting version is:

write a handler
insert an order row
return 201
manually click the route once
ship it

That feels like progress because the happy path works.

It breaks when ordinary backend pressure arrives:

the client retries after a timeout
two requests arrive close together
the product price changes
the user tries to read someone else's order
the migration runs before every instance is updated
support asks what happened to request req_982

The happy-path handler cannot answer those questions by itself.

The missing piece is not a framework feature. The missing piece is an end-to-end chain where each layer reinforces the same promise.

Check: Why is "the handler returned 201 once" weak evidence for this capstone?

Think first, then reveal.

Answer: It proves only one local happy path. It does not prove authorization, validation, durable constraints, retry behavior, migration compatibility, release readiness, or production evidence.

Walkthrough: One Request End to End

Trace POST /orders as a review, not as code trivia.

Input:
  user u17
  role customer
  idempotency key key_abc
  body asks for 2 units of product p1

Step 1: route and contract
  route matches POST /orders
  service expects JSON body and Idempotency-Key header

Intermediate state:
  the backend knows which operation the client requested

Step 2: authentication and authorization
  token maps to user u17
  policy allows customers to create their own orders

Intermediate state:
  the request has an actor and an allowed action

Step 3: validation
  product_id is present
  quantity is a positive integer
  client did not send trusted price_cents

Intermediate state:
  input shape is acceptable, but no durable state has changed

Step 4: transaction
  find active product p1
  read price from database
  compute total from database price
  insert order for u17
  insert order item
  enforce unique (user_id, idempotency_key)
  commit

Intermediate state:
  order and items are durable together

Step 5: response
  return 201 with order id, status, and total

Step 6: evidence
  integration test proves retry behavior
  release artifact has version abc123
  log, metric, trace, and event explain the request

Output:
  one order exists
  duplicate retry does not create another order
  support can investigate the request later

Now compare the broken version:

Input:
  same request arrives twice after a client timeout

Naive transition:
  handler inserts order without idempotency key
  handler inserts another order on retry

Output:
  customer may be charged or fulfilled twice
  support sees two orders but cannot tell which one is the retry

The capstone standard is not "add idempotency as a word." It is:

contract requires key
handler uses key
database enforces uniqueness
integration test retries the route
observability emits idempotency_replayed or conflict evidence

So far, the service is becoming coherent. One behavior is visible across API, code, database, tests, and operations.

Evidence Before Release

A small backend needs evidence before production traffic reaches it.

Use the right evidence for the right boundary.

unit tests:
  total calculation
  validation helper
  role decision helper
  domain error to HTTP status mapping

integration tests:
  real route
  auth middleware
  database transaction
  foreign keys and uniqueness constraints
  migration-applied schema
  idempotent retry through HTTP

contract checks:
  request schema
  response body
  expected status codes
  stable error shape

release checks:
  migration runs from previous schema
  container starts with required configuration
  artifact is tagged with commit or digest
  rollout watches route success and error rate

A handler-only unit test is still useful. It is just not enough.

For POST /orders, the strongest beginner-level test is an integration test:

start test server
apply migrations
create user and product fixtures
send POST /orders with idempotency key
assert response is 201
assert one order row and one item row exist
send same request again with same key
assert existing order is returned or replayed safely
assert no duplicate order exists
send same key with different body
assert 409 Conflict

That test proves the route, middleware, transaction, database constraint, and response mapping together.

Check: Which layer should make duplicate order creation impossible: client, handler, database, or test?

Think first, then reveal.

Answer: The design should use all of them together. The client sends an idempotency key, the handler uses it, the database enforces uniqueness, and tests prove the behavior through the real route.

Failure Review

A capstone should review failure before release, not after surprise.

Here are useful failure questions for POST /orders:

Bad JSON:
  Do we return 400 with a stable error body?

Missing token:
  Do we return 401 instead of creating anonymous state?

Wrong user:
  Can one user read or modify another user's order?

Inactive product:
  Do we reject the order before writing partial state?

Duplicate retry:
  Do we replay safely or return a clear conflict?

Database unavailable:
  Do we fail clearly and emit dependency evidence?

Migration missing:
  Does deployment fail fast instead of serving broken traffic?

Payment provider slow:
  Does the API behavior match the contract, and can operators see the dependency delay?

The important pattern is:

failure -> expected response -> durable state -> evidence

For example:

failure:
  same idempotency key, different body

expected response:
  409 Conflict

durable state:
  no new order is created

evidence:
  log event=idempotency_conflict request_id=req_912 route=POST /orders
  metric idempotency_conflicts_total{route=POST /orders} += 1
  trace shows transaction rolled back

That is stronger than "an error happened." It says what the service promised, what state changed, and what evidence remains.

Trade-offs

The central trade-off is shipping speed versus confidence under real conditions.

You can ship faster by skipping API contracts, database constraints, integration tests, migration checks, release metadata, and observability. The service may look done sooner. The cost is that uncertainty moves into production.

This capstone discipline improves:

It costs:

It can still fail when:

You can see the boundary when the team can answer local code questions but cannot answer production questions:

Which version handled the failed request?
Did the transaction commit?
Was this a retry?
Which invariant protected the data?
Which test would fail if this behavior broke?

The goal is not perfect certainty. It is enough evidence to make risk visible before users do the discovery work.

Evidence and Readiness

Use this readiness table before calling the API shippable.

Boundary Ready Signal Not Ready Signal
Contract Important routes have request, response, and error shapes Clients must read handler code to guess behavior
Identity Backend authorizes from authenticated user and action Frontend hides buttons and backend trusts it
Validation Invalid input returns stable 400-style errors Bad input reaches database or becomes 500
Durable state Constraints protect facts that must stay true Correctness depends only on application hope
Retry Idempotency behavior is designed and tested Duplicate requests create duplicate orders
Tests Integration test crosses route and database boundary Only handler helpers are tested
Configuration Secrets and environment-specific values are outside code Local config is baked into the artifact
Release Artifact, migration, and rollout checks are repeatable Deployment depends on manual local steps
Observability Logs, metrics, traces, and events explain one request Support cannot connect user report to backend path

This table is deliberately practical. It does not ask whether the service uses a fashionable architecture. It asks whether the important behavior can survive contact with users.

Use the table from left to right.

Start with the contract because every later layer depends on the promise. If the contract is vague, the database cannot know which facts matter, tests cannot know what to assert, and observability cannot know which signals describe success.

Then check identity and validation. These are the first trust boundaries. A request from the internet is not yet a command the service should obey. The backend must decide who is acting, whether the action is allowed, and whether the input shape is acceptable.

Only then review durable state. This is where beginner services often look fine until concurrency or retry pressure appears. The database should protect facts that must remain true even when two requests arrive at the same time. Application code is still important, but durable constraints are the final guardrail for durable facts.

After that, review tests and release. A service that works on one laptop is not the same as a service that can be rebuilt, migrated, deployed, and rolled out with evidence. The artifact should be repeatable. The migration should be compatible. The release should have a stop signal.

Finally, review observability. This is where the service proves it can be operated after the happy path. If a user says "my order failed," the team should not need to guess. The request ID, route, release, database outcome, dependency outcome, and domain event should give a first investigation path.

That order matters. It keeps the capstone from becoming a vague checklist. Each layer asks a more specific version of the same question:

What did we promise?
How do we enforce it?
How do we prove it?
How do we explain it later?

A Review Walkthrough

Imagine a teammate says:

I finished POST /orders.
Can you review whether it is ready?

A weak review looks only at the handler:

Does the code look clean?
Does it return 201?

A stronger review follows the chain.

First, ask for the contract. You want to see the request body, required headers, success response, and error responses. If the idempotency key is optional, ask what happens when the client retries. If error bodies are inconsistent, ask how clients will recover.

Second, ask about identity. Which user is authenticated? Which role may create an order? Is authorization enforced on the backend, or only implied by the frontend? The backend should not trust a hidden button. A user can call an API without using your UI.

Third, ask about validation. The client can send malformed JSON, negative quantities, unknown product IDs, or fields the service should ignore. The handler should reject bad input before writing durable state. It should not let accidental input become a partial order.

Fourth, ask about the transaction. Where does the service read the product price? When does it compute the total? What happens if one item is invalid? What happens if the second insert fails? The answer should include a transaction boundary and database constraints, not only hopeful application code.

Fifth, ask about retry. A timeout does not mean the server did nothing. The client may retry after the order already committed. The service needs a stable idempotency rule so the same request does not create a second order.

Sixth, ask about evidence before release. Which test crosses the real route and database boundary? Which test proves duplicate retry behavior? Which check proves the migration path works from the previous schema?

Seventh, ask about evidence after release. Which log fields would support need? Which metric would show user-visible harm? Which trace span would show whether the database or payment provider was slow?

This review style is slower than glancing at the handler. It is also the difference between "the code looks plausible" and "the service behavior is explainable."

Check: During review, you find that POST /orders has a contract and unit tests, but no integration test through the route and database. What risk remains?

Think first, then reveal.

Answer: The handler logic may be tested, but the real boundary may still fail. Middleware, auth, validation wiring, migrations, database constraints, transactions, and HTTP response mapping may not work together.

Final Challenge

Review this proposed implementation:

POST /orders:
  accepts JSON body with product_id and quantity
  trusts price_cents from the client
  creates an order row before inserting items
  has a unit test for total calculation
  has no idempotency key
  logs "order failed" on exceptions
  deploys from a developer laptop

Use the capstone rubric. Name at least six problems and propose a safer replacement for each.

A strong answer should mention:

Now do the same review for PATCH /admin/products/:id.

Ask:

Who is allowed?
What fields may change?
Which database facts must remain true?
What response does the client expect?
Which tests prove denied and allowed cases?
What signal explains a failed admin update?

If you can answer those questions for a second route, you are not just memorizing this capstone. You are transferring the backend review method.

That transfer is the point of the track. The exact tables, routes, framework, and database will change from project to project. The review shape should remain familiar: name the promise, protect the state, prove the behavior, ship the artifact, and leave evidence. When a new backend feels messy, return to one request and make the chain visible again.

Resources

Key Takeaways

PREVIOUS Observability Basics for Backend Services