Call
APIs

How to Build a REST API: Contract-First Design, Backend Layers, Testing, and Production Rollout

A practical REST API implementation guide from resource and OpenAPI contract design through handlers, service/repository boundaries, validation, authorization, idempotency, database transactions, contract tests, observability, and safe rollout.

Published: December 30, 2024
Updated: September 12, 2026
15 min read
How to Build a REST API: Contract-First Design, Backend Layers, Testing, and Production Rollout

How to Build a REST API: Contract-First Design, Backend Layers, Testing, and Production Rollout

Building a REST API is not primarily about choosing a web framework. The hard part is turning a product capability into a stable network contract that remains understandable, secure, testable, and compatible while the implementation changes underneath it.

A useful implementation sequence is:

  1. define the resource and business behavior
  2. design the HTTP contract
  3. describe that contract in OpenAPI
  4. implement transport, business, and persistence boundaries
  5. enforce validation and authorization
  6. design transactions, idempotency, and failure behavior
  7. test the contract and real integrations
  8. add observability and operational limits
  9. roll out compatibly

This guide focuses on that build process. REST fundamentals, general REST best practices, and API versioning are covered separately on Lofingo.

1. Start from the product capability

Before creating routes, write down what the user or integration actually needs to do.

Suppose the requirement is:

Customers can create orders, inspect an order, list their recent orders,
and cancel an order before fulfillment begins.

That is more useful than beginning with database tables.

Now identify the domain resources:

Order
Customer
OrderItem

Then define the important operations and invariants:

create order
get one order
list orders for current customer
cancel order if current state allows cancellation

This keeps the API oriented around product behavior instead of exposing internal CRUD mechanically.

2. Design stable resource URIs

A straightforward resource model could use:

POST /orders
GET /orders/{order_id}
GET /orders
POST /orders/{order_id}/cancellation

or a PATCH-based cancellation model if cancellation is naturally represented as a state transition in your contract.

The exact style matters less than consistency and semantics.

Do not expose implementation concepts such as:

/runCreateOrderStoredProcedure
/getOrderFromPrimaryDatabase

The public URI should survive internal refactors.

3. Write request and response examples before writing handlers

For order creation, define the request:

POST /orders
Content-Type: application/json
Idempotency-Key: 01J...
{
  "items": [
    {
      "product_id": "p_42",
      "quantity": 2
    }
  ],
  "shipping_address_id": "addr_7"
}

Then define success:

HTTP/1.1 201 Created
Location: /orders/o_900
Content-Type: application/json
{
  "id": "o_900",
  "status": "pending",
  "total": {
    "amount_minor": 998000,
    "currency": "INR"
  }
}

And define failure cases before implementation:

invalid quantity
unknown product
insufficient stock
shipping address belongs to another customer
duplicate idempotency key with different payload

Thinking through error semantics early prevents every handler from inventing its own response later.

4. Use OpenAPI as an executable contract

The current OpenAPI Specification is 3.2.0, published in September 2025.

OpenAPI provides a machine-readable description of an HTTP API that can power:

  • reference documentation
  • client generation
  • server stubs
  • schema validation
  • contract tests
  • mock servers
  • linting
  • security review

A simplified path definition might look like:

openapi: 3.2.0
info:
  title: Orders API
  version: 1.0.0

paths:
  /orders/{order_id}:
    get:
      operationId: getOrder
      parameters:
        - name: order_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Order found
        '404':
          description: Order not found

The OpenAPI document does not replace implementation tests. It gives the organization one explicit contract to validate against.

5. Choose one contract ownership model

Two common workflows are valid.

Contract-first

Design OpenAPI before implementation, review it, then generate or implement server/client code from the approved contract.

This works well when:

  • several teams depend on the API
  • client and server work happen in parallel
  • external consumers need early review

Code-first with contract verification

Implement route/types in code and generate OpenAPI from source annotations/types.

This can work well when:

  • one backend team owns the whole surface
  • framework tooling is reliable
  • CI verifies generated output does not drift unexpectedly

The dangerous model is neither: hand-maintaining documentation separately from production behavior.

Pick one source of truth and test it.

6. Keep transport code thin

A route/controller/handler should mainly translate HTTP into an application operation.

Conceptually:

HTTP handler
  parse request
  authenticate caller context
  validate transport/schema input
  call application service
  translate result/error to HTTP response

Avoid putting the full business workflow inside the web handler:

HTTP parsing
SQL queries
inventory rules
payment logic
email sending
authorization
response formatting

all in one function.

Thin transport boundaries make the same business operation easier to reuse from jobs, gRPC, CLI/admin tools, or tests without pretending HTTP is the domain model.

7. Put business invariants in an application/domain layer

An order service might own logic such as:

customer may use this shipping address
product is currently purchasable
quantity is allowed
order can transition from pending to cancelled

The HTTP handler should not be the only place those rules exist.

Otherwise, a background worker or second API endpoint can bypass them accidentally.

This does not require heavy “enterprise architecture.” A small service module with clear responsibility is enough.

8. Keep persistence behind explicit data-access boundaries

Your service may need operations such as:

get product for update
create order
list customer orders by cursor
store idempotency result

Whether those operations use PostgreSQL, MySQL, DynamoDB, or another store is an implementation detail.

A repository/query layer is useful when it makes query ownership and testing clearer.

Do not introduce repository abstractions whose only job is renaming every ORM method one-to-one. The abstraction should represent useful application data access, not ceremony.

9. Validate transport shape before business logic

Reject malformed input before executing expensive business work.

Validate:

  • content type
  • body size
  • JSON syntax
  • required properties
  • field types
  • string lengths
  • enum values
  • numeric ranges
  • array lengths

Example product rules:

items must contain 1 to 100 entries
quantity must be 1 to 1000
address_id must be a valid identifier format

A type such as string alone does not protect the service from a 50 MB text field.

Schema validation should include real bounds.

10. Treat authorization as a lookup constraint

One of the highest-risk API mistakes is broken object-level authorization.

Suppose the request is:

GET /orders/o_900

A weak implementation does:

load order o_900
then maybe check whether caller owns it

A safer application query is conceptually:

load order o_900 that is visible to authenticated customer c_42

or apply one centralized authorization policy immediately after lookup and before any sensitive data leaves the domain boundary.

OWASP's API Security Top 10 highlights broken object-level authorization because identifier-based APIs make this mistake easy to introduce.

Authentication proves identity. It does not automatically authorize access to arbitrary object IDs.

11. Separate tenant context from client-controlled fields

For a multi-tenant API, do not let a normal caller choose arbitrary tenant ownership through request JSON:

{
  "tenant_id": "someone-elses-tenant"
}

when tenant identity should come from the authenticated session/API key.

Derive trusted context from authentication and use it to scope:

  • reads
  • writes
  • idempotency keys
  • cache keys
  • rate limits
  • audit records

Client-supplied resource IDs are inputs, not authority.

12. Make database transactions match the business boundary

Order creation may need several durable changes:

reserve/decrement inventory
create order
create order items
record outbox event
store idempotency result

When they must succeed or fail together and live in one transactional database, put them in one database transaction.

Keep the transaction short.

Do not hold it open while waiting for:

  • payment provider HTTP requests
  • email provider
  • image processing
  • unrelated remote services

Cross-system workflows require different reliability patterns.

13. Use an outbox for reliable post-commit events

Suppose order creation should emit:

OrderCreated

This is unsafe:

commit order
publish event

because the process can crash after commit but before publish.

A transactional outbox writes both:

order rows
outbox event row

inside one database transaction.

A worker later publishes the outbox record and marks/records delivery progress.

Downstream consumers should be idempotent because delivery retries can occur.

This is more reliable than making the API request synchronously call every downstream system.

14. Design POST idempotency before clients need retries

Network outcomes can be ambiguous.

The server may commit successfully and lose the response before the client receives it.

For important create/action requests, accept a stable idempotency key:

Idempotency-Key: checkout_01J...

Within the correct user/tenant scope:

  1. first request atomically claims/stores the key
  2. business operation runs
  3. canonical result is associated with the key
  4. exact retry returns the original result
  5. same key with incompatible request body is rejected

Do not implement this as an unsafe “check then insert” race.

Use a unique constraint or equivalent atomic primitive.

15. Decide synchronous vs asynchronous work explicitly

Some operations should return quickly with final state:

create address
update profile
create small order

Others naturally take longer:

generate large export
transcode video
run data import

For long work, create a job resource:

POST /exports

return:

202 Accepted
Location: /export-jobs/job_42

Then expose:

GET /export-jobs/job_42

with states such as:

queued
working
completed
failed

This is easier to retry and observe than holding a request open for several minutes unnecessarily.

16. Build pagination into collection endpoints from day one

This route should not silently return every order:

GET /orders

Use bounded pagination.

For a large mutable collection, cursor pagination might look like:

GET /orders?limit=50&after=opaque_cursor

The server enforces:

default limit: 20
maximum limit: 100

The cursor should correspond to a stable indexed order.

Pagination is part of the public contract; adding it only after the endpoint has millions of rows can break clients that assumed complete results.

17. Keep filter and sort options intentionally finite

Do not turn a REST endpoint into arbitrary SQL through parameters such as:

filter=
sort=

Define supported fields:

status
created_after
created_before
sort=created_at
sort=-created_at

Then design indexes around important combinations.

This improves performance predictability and prevents attackers or accidental clients from constructing extremely expensive queries.

18. Use one error contract across the API

RFC 9457 defines Problem Details for HTTP APIs.

A domain conflict can return:

HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
  "type": "https://api.example.com/problems/order-not-cancellable",
  "title": "Order cannot be cancelled",
  "status": 409,
  "detail": "Fulfillment has already started",
  "error_code": "ORDER_NOT_CANCELLABLE"
}

Clients branch on stable machine-readable semantics rather than exact English text.

Internal stack traces and SQL errors belong in server logs, not public responses.

19. Map domain errors to HTTP at the boundary

Your service layer can return domain outcomes such as:

OrderNotFound
OrderNotCancellable
InsufficientStock
PermissionDenied

The HTTP adapter maps them to status + problem response.

This avoids spreading HTTP status codes throughout core business logic.

It also makes the domain easier to expose through a different transport later if needed.

20. Generate safe client SDKs from the contract where useful

OpenAPI can generate typed clients, but generated SDKs should be treated as product code.

Review:

  • naming
  • optional/null fields
  • enum compatibility
  • pagination helpers
  • error types
  • authentication setup
  • retry defaults
  • timeout defaults

A generated SDK with unsafe automatic retries can create worse behavior than a handwritten thin client.

Generation removes boilerplate; it does not remove API design responsibility.

21. Test at several layers

One test type cannot prove the whole API.

Unit tests

Test pure business rules quickly:

pending order can cancel
shipped order cannot cancel

Repository/integration tests

Test real SQL constraints, transactions, queries, and migrations against the supported database engine.

HTTP integration tests

Start the real route stack and verify:

  • authentication
  • request parsing
  • status codes
  • headers
  • response schema

Contract tests

Verify production behavior matches OpenAPI and does not introduce unintended breaking changes.

End-to-end tests

Cover a small number of critical full workflows through realistic dependencies.

Do not replace all focused tests with slow end-to-end suites.

22. Test authorization negatively

Security tests should prove what a user cannot access.

For object-level access:

customer A can read A's order
customer A cannot read B's order
customer A cannot update B's order
admin permission behaves according to policy

Test sequential and guessed identifiers, nested subresources, bulk endpoints, exports, and alternate methods.

A happy-path authenticated test does not prove authorization is correct.

23. Test idempotency under concurrency

Do not test only two sequential duplicates.

Send the same idempotency key concurrently and verify exactly one logical operation is created.

Also verify:

  • same key + same payload returns same logical result
  • same key + different payload is rejected
  • expired key behavior is documented
  • tenant/user scope cannot collide accidentally

Concurrency is where naive idempotency implementations usually fail.

24. Add compatibility checks to CI

Compare the current OpenAPI document against the last released contract.

Flag changes such as:

  • removed paths
  • removed response properties
  • newly required request fields
  • incompatible type changes
  • changed enum behavior
  • changed security requirements

Some flagged changes may be intentional. The important point is that breaking changes become an explicit review decision rather than an accidental merge.

25. Run migrations separately but compatibly

A production API release may run with old and new application instances simultaneously.

Use expand/contract database changes where possible:

add compatible schema
release code that understands old and new
backfill data in bounded batches
switch reads/writes
remove old schema later

Avoid a release that requires every server and database change to become active in the exact same millisecond.

API availability depends on migration compatibility too.

26. Put operational limits in configuration

A production API should have explicit limits for:

  • request body size
  • header size through your infrastructure
  • page size
  • upload size
  • request timeout
  • database statement timeout
  • downstream call timeout
  • concurrent expensive jobs
  • rate limits and quotas

OWASP's API Security Top 10 includes unrestricted resource consumption because valid-looking API requests can still exhaust compute, memory, storage, bandwidth, or paid downstream services.

Limits are product behavior, not just infrastructure knobs.

27. Rate-limit by meaningful caller and operation

Possible dimensions include:

anonymous IP
user
API key
OAuth application
tenant
operation cost class

A large enterprise tenant and an anonymous login endpoint may need completely different policies.

Return 429 Too Many Requests for enforced request-rate limits and document retry behavior.

Also protect expensive business flows such as:

  • password reset
  • SMS OTP
  • export generation
  • AI/media processing

where the real risk is business-resource exhaustion rather than raw HTTP request count.

28. Add route-level metrics before launch

Track normalized route templates:

GET /orders/{id}
POST /orders
GET /orders

not individual customer IDs.

Useful dimensions include:

  • request count
  • status code/class
  • p50/p95/p99 latency
  • request/response bytes
  • auth failures
  • validation failures
  • rate-limit rejects
  • database latency
  • downstream dependency latency

Avoid user IDs, tokens, raw URLs, and free-form search input as metric labels.

29. Use distributed tracing for dependency-heavy endpoints

A trace for order creation should show meaningful operations such as:

POST /orders
  authenticate
  validate request
  inventory database transaction
  order transaction
  outbox insert

If the request calls remote services, trace those calls too.

Do not create a span for every trivial helper function; trace boundaries that explain latency and failure.

A trace should help answer where the request spent its time.

30. Log enough context without logging secrets

Useful structured log fields include:

  • request/trace ID
  • normalized route
  • method
  • status
  • duration
  • application error code
  • service version

Be careful with:

  • Authorization headers
  • cookies
  • API keys
  • passwords
  • payment data
  • full private request/response bodies

Debugging value does not justify leaking credentials into permanent logs.

31. Roll out breaking-risk changes gradually

For a risky implementation change behind the same API contract:

  • deploy to a small percentage of instances
  • compare latency/errors
  • keep rollback easy
  • watch business correctness metrics

For an actual new API version:

  • run old and new contracts in parallel
  • publish migration guidance
  • measure client adoption
  • deprecate and sunset deliberately

Do not use API versioning to avoid doing safe internal rollouts.

32. Keep OpenAPI and runtime behavior synchronized after release

In CI or post-deployment checks, validate representative operations against the published contract.

Watch for drift such as:

OpenAPI says field nullable
runtime rejects null

OpenAPI says 404
runtime returns 200 with error body

OpenAPI documents cursor pagination
runtime silently uses offset

The contract only creates value if consumers can trust it.

A practical project structure

A small service might use a structure conceptually like:

http/
  handlers
  request/response mapping
  middleware

application/
  order service
  authorization policies

domain/
  entities/value objects
  business rules

persistence/
  SQL queries/repositories

integration/
  payment/inventory clients

api/
  OpenAPI contract

Do not copy this structure mechanically.

The principle is to keep transport, business rules, persistence, and external dependencies separable enough to reason about and test.

Production checklist

Before calling a new REST API production-ready, verify:

  • resource and workflow behavior are defined before route implementation
  • request/response/error examples are reviewed
  • OpenAPI matches runtime behavior
  • transport handlers do not own all business logic
  • schema and business validation have explicit limits
  • object/tenant authorization is tested negatively
  • local durable changes use correct transaction boundaries
  • external side effects use reliable async/outbox patterns where needed
  • retryable mutations have concurrency-safe idempotency
  • collection endpoints have bounded pagination/filtering
  • errors follow one stable machine-readable contract
  • unit, integration, contract, and critical end-to-end tests exist
  • breaking contract changes are detected in CI
  • request size, timeout, rate, and cost limits are configured
  • metrics/traces/logs identify failures without leaking secrets
  • database migrations and API rollouts are backward compatible

A production REST API is successful when the contract remains stable while the implementation stays free to improve. Designing that boundary deliberately is more important than which HTTP framework created the route.

References

Tags:RESTAPI DevelopmentOpenAPIBackend ArchitectureAPI TestingProduction Engineering
ENTERPRISE SAAS ENGINEERING

BUILDING A SAAS PRODUCT?

Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.

EXPLORE SAAS SERVICES →