Skip to content
API Design and Integration: The Practical Handbook
Exit

API Design and Integration: The Practical Handbook

API Design Handbook

by Pronab Roy

A 12-chapter, book-style handbook on designing, securing, and integrating production-grade APIs, with examples in ASP.NET Core.

Start reading

Chapter 1 Part I: Foundations

What Makes a Good API? REST and HTTP Fundamentals

Every API is a promise. When you publish an endpoint, you are telling other developers: “Send me this, and I’ll send you that, today and next month.” Good APIs keep that promise in a way that feels obvious to the people using them. Poor APIs keep it on a technicality, or break it quietly.

This first chapter lays the foundation for the rest of the handbook. We’ll look at APIs as products, unpack what REST really means (and what it doesn’t), take apart an HTTP message piece by piece, and finish by writing the first endpoint of ShipCart, the e-commerce, logistics, and SaaS system we’ll grow throughout the series.

APIs as Products: Consumers, Contracts, and Developer Experience

It’s tempting to think of an API as “the layer in front of the database.” That view leads to APIs shaped around tables and internal code instead of around the people calling them.

A more useful view is to treat an API as a product with three parts:

  • Consumers. Someone calls your API: a storefront front end, a mobile app, a carrier’s system, a merchant’s back office, or another team’s service. Each has different needs, different release cycles, and different tolerance for change.
  • The contract. This is everything a consumer can rely on: URLs, methods, request and response shapes, status codes, headers, error formats, and behaviour. If a consumer can observe it, they may come to depend on it, whether or not you wrote it down.
  • Developer experience (DX). How quickly can a new developer make a successful first call? Are names predictable? Do errors explain what went wrong and how to fix it? DX is the API equivalent of usability.

In ShipCart, the storefront team wants fast, cache-friendly product reads. A logistics partner wants a stable way to push tracking events. Merchants on the SaaS platform want clear API keys, clear limits, and docs they can follow without opening a support ticket. Designing with all three in mind is the difference between an API people put up with and one they like using.

REST Constraints, Explained

REST (Representational State Transfer) comes from Roy Fielding’s 2000 doctoral dissertation, Architectural Styles and the Design of Network-based Software Architectures (Chapter 5). It is not a protocol or a standard. It is an architectural style: a set of constraints that, together, give a system properties such as scalability, visibility, and independent evolution.

The constraints are:

  1. Client–server. The client handles the user interface and user workflow; the server handles data and business rules. Separating them lets the ShipCart storefront and the ShipCart API evolve and deploy independently.
  2. Statelessness. Each request carries everything the server needs to understand it. The server does not keep per-client session state between requests. Authentication tokens, pagination cursors, and filters travel with each request. This makes it easy to put many server instances behind a load balancer, because any instance can handle any request.
  3. Cacheability. Responses must say whether they can be cached and for how long. A product listing that changes a few times a day is a good candidate for caching; a cart total is not. We’ll cover Cache-Control and ETag in Chapter 3.
  4. Uniform interface. This is the core of REST. It has four parts: resources are identified by URIs; clients manipulate resources through representations (for example, a JSON document describing a product); messages are self-descriptive (methods, status codes, and media types carry standard meaning); and hypermedia as the engine of application state (HATEOAS), meaning responses contain links that tell the client what it can do next.
  5. Layered system. A client can’t tell, and shouldn’t need to know, whether it’s talking to the origin server or to a gateway, CDN, or reverse proxy in between. This is what lets you add a cache or a rate-limiting gateway without changing clients.
  6. Code on demand (optional). Servers may send executable code to clients. It’s rarely relevant for JSON APIs.

Why HATEOAS Is Often Skipped

Hypermedia is the constraint most “REST APIs” leave out. In practice, most API clients are written by developers who read the documentation and hard-code the URLs and flows they need, rather than discovering them from links at run time. Adding rich hypermedia then costs design effort without changing how clients behave.

That doesn’t make links worthless. A next link in a paginated response, or a Location header pointing to a newly created order, are small, practical uses of hypermedia, and we’ll use both in this series. Just be honest about what you’re building: an HTTP API that follows most REST constraints, not a fully hypermedia-driven one.

HTTP Anatomy: The Message on the Wire

REST describes a style; HTTP is the protocol almost every REST-style API uses. HTTP’s meaning, including methods, status codes, and headers, is defined in RFC 9110 (HTTP Semantics). How HTTP/1.1 messages are formatted on the wire is defined in RFC 9112.

An HTTP/1.1 request has:

  • A request line: method, request target, and protocol version, for example GET /products HTTP/1.1.
  • Header fields: name–value pairs such as Host, Accept, and Authorization.
  • An empty line.
  • An optional body (content), for example the JSON of a new order in a POST.

A response has:

  • A status line: protocol version, status code, and reason phrase, for example HTTP/1.1 200 OK.
  • Header fields such as Content-Type, Cache-Control, and Date.
  • An empty line.
  • An optional body.

HTTP/2 and HTTP/3 encode messages differently (binary frames instead of text lines), but the semantics are the same. A GET is a GET and a 404 is a 404 whichever version carries it.

Content Negotiation: Accept and Content-Type

Two headers cause a lot of confusion:

  • Content-Type describes the body of the message it appears on. A client sending JSON in a POST sets Content-Type: application/json; a server returning JSON sets the same on the response.
  • Accept is sent by the client to say which media types it is willing to receive, such as Accept: application/json.

When a server can’t produce any representation the client accepts, RFC 9110 defines 406 Not Acceptable. When a client sends a body in a format the server doesn’t support, the matching status is 415 Unsupported Media Type. As you’ll see in the exercise, frameworks don’t always enforce these for you, so it’s worth knowing what your framework actually does.

REST vs RPC-Style vs “REST-ish”

Most real-world APIs fall somewhere on a spectrum:

  • RPC-style APIs expose operations: POST /getProducts, POST /createShipment. Everything is often a POST, and errors may come back as 200 OK with an error flag in the body. It’s simple to start with, but caches and proxies can’t understand what’s happening, and every client must learn your custom conventions.
  • Resource-oriented (“REST-ish”) APIs expose resources and use HTTP semantics: GET /products, POST /shipments, 404 when a product doesn’t exist. Most public HTTP APIs you use today look like this, usually without HATEOAS.
  • Fully RESTful APIs follow every constraint, including hypermedia. They are less common in typical JSON APIs.

RPC is not wrong. Purpose-built RPC frameworks such as gRPC are excellent for internal service-to-service calls, and we’ll compare them properly in Chapter 12. The problem is accidental RPC over HTTP: using HTTP as a tunnel while ignoring the meaning it already provides. In this handbook, we build resource-oriented APIs that respect HTTP semantics, and we use pragmatic hypermedia where it helps.

Qualities of a Good API

Across all the chapters ahead, four qualities keep coming back:

  • Consistency. The same concept looks the same everywhere. If ShipCart uses createdAt on orders, it doesn’t use creation_date on shipments. Errors share one format. Collections paginate the same way.
  • Predictability. A developer who has used two endpoints can guess the third. GET /products/{id} suggests GET /orders/{id}. Status codes mean what HTTP says they mean.
  • Discoverability. It’s easy to find out what exists and how to use it: good documentation, an OpenAPI description, meaningful error messages, and helpful links such as Location and next.
  • Evolvability. The API can grow without breaking existing consumers. Adding a field is safe; renaming one is not. We’ll spend all of Chapter 7 on this.

None of these depends on a framework. They are design decisions, and they’re far cheaper to get right at the start than to retrofit later.

A First Look at ASP.NET Core: Minimal APIs vs Controllers

ASP.NET Core offers two fully supported ways to build HTTP APIs:

  • Minimal APIs map routes directly to handlers (lambdas or methods) with app.MapGet, app.MapPost, and so on. There is very little ceremony, and route groups (MapGroup) and endpoint filters help as the API grows.
  • Controllers group actions into classes that inherit from ControllerBase, using attributes such as [ApiController] and [HttpGet]. They come with conventions many teams already know, such as model binding conventions and action filters.

When to pick which: For new, focused services like ShipCart, Minimal APIs are a great fit: less code, explicit wiring, and good performance. Controllers make sense when your team already has a large controller-based codebase or relies on features and conventions built around MVC. Both use the same routing, dependency injection, middleware, and hosting, and the API design principles in this handbook apply equally to both. We’ll use Minimal APIs throughout.

Hands-On Exercise: Create ShipCart and Add GET /products

You’ll need the .NET SDK (current LTS) and curl. The code below works on .NET 8 and later.

Step 1: Create the solution

mkdir shipcart && cd shipcart
dotnet new sln -n ShipCart
dotnet new web -n ShipCart.Api -o src/ShipCart.Api
dotnet sln add src/ShipCart.Api

The web template gives you an empty ASP.NET Core app, which is a clean starting point for a Minimal API.

Step 2: Add the endpoint

Replace the contents of src/ShipCart.Api/Program.cs with:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// A tiny in-memory catalogue. A real database arrives in later chapters.
var products = new List<Product>
{
    new(1, "SKU-1001", "Canvas Tote Bag", 19.99m, "USD", InStock: true),
    new(2, "SKU-1002", "Stainless Water Bottle", 24.50m, "USD", InStock: true),
    new(3, "SKU-1003", "Wireless Mouse", 29.00m, "USD", InStock: false)
};

// GET /products returns the whole catalogue as JSON.
app.MapGet("/products", () => TypedResults.Ok(products))
   .WithName("ListProducts");

app.Run();

// The shape clients see. Keep it separate from any database entity.
public record Product(
    int Id,
    string Sku,
    string Name,
    decimal Price,
    string Currency,
    bool InStock);

A few things to notice:

  • Product is a record that defines the representation clients receive. Starting with a dedicated type, rather than exposing a database entity, is a habit we’ll build on in Chapter 4.
  • TypedResults.Ok(...) makes the 200 OK response explicit in the handler’s return type. Chapter 3 shows how this scales to several possible responses.
  • ASP.NET Core serialises the list with System.Text.Json, using camelCase property names by default.

Step 3: Run it

dotnet run --project src/ShipCart.Api --urls http://localhost:5000

The --urls option pins the port so the commands below match. Without it, the app uses the port in Properties/launchSettings.json, which the template picks at random.

Step 4: Inspect the raw HTTP exchange

In a second terminal:

curl -v http://localhost:5000/products

You’ll see output similar to this (your curl version, date, and connection details will differ):

* Host localhost:5000 was resolved.
*   Trying [::1]:5000...
* Connected to localhost (::1) port 5000
> GET /products HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/8.14.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 17 Oct 2026 19:56:33 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
[{"id":1,"sku":"SKU-1001","name":"Canvas Tote Bag","price":19.99,"currency":"USD","inStock":true},{"id":2,"sku":"SKU-1002","name":"Stainless Water Bottle","price":24.50,"currency":"USD","inStock":true},{"id":3,"sku":"SKU-1003","name":"Wireless Mouse","price":29.00,"currency":"USD","inStock":false}]

Here’s how to read it. curl prefixes its own messages with *, what it sends with >, and what it receives with <.

Line(s) Part What it means
* Host … / Trying … / Connected … curl info DNS lookup and TCP connection. Not part of HTTP itself.
> GET /products HTTP/1.1 Request line Method GET, target /products, protocol HTTP/1.1.
> Host: localhost:5000 Request header Required in HTTP/1.1; tells the server which host the client wants.
> User-Agent: curl/8.14.1 Request header Identifies the client software.
> Accept: */* Request header Content negotiation: curl accepts any media type.
> (empty) End of headers A GET has no body, so the request ends here.
< HTTP/1.1 200 OK Status line Version, status code 200, reason phrase OK.
< Content-Type: application/json; charset=utf-8 Response header Describes the body: JSON, encoded as UTF-8.
< Date: … Response header When the server generated the response, always in GMT.
< Server: Kestrel Response header The ASP.NET Core web server. (Many teams remove this in production.)
< Transfer-Encoding: chunked Response header HTTP/1.1 framing: the body is streamed in chunks instead of declaring a Content-Length up front.
< (empty) End of headers The body follows.
[{"id":1, …}] Body The representation of the /products collection.

Step 5: Explore and label

Try these, and for each one write down the request line, the status line, and any headers that changed:

  1. Send a different method: curl -i -X POST http://localhost:5000/products. You should get 405 Method Not Allowed with an Allow: GET header: the route exists, but not for that method.
  2. Ask for a resource that doesn’t exist: curl -i http://localhost:5000/carts. You’ll get 404 Not Found.
  3. Ask for XML: curl -i -H "Accept: application/xml" http://localhost:5000/products. Notice that you still get 200 OK with JSON. Minimal APIs return JSON and don’t perform content negotiation for you. Is that acceptable for ShipCart? (For a JSON-only API, many teams decide it is. Write down your reasoning.)
  4. Stretch goal: Add GET /products/{id:int} that returns TypedResults.Ok(product) or TypedResults.NotFound(). Hint: because the lambda can return two different types, declare its return type as Results<Ok<Product>, NotFound> (add using Microsoft.AspNetCore.Http.HttpResults;). Check both cases with curl -i.

Key Takeaways

  • An API is a product with consumers and a contract. Design for the people calling it, not for your database.
  • REST is an architectural style defined by constraints: client–server, stateless, cacheable, uniform interface, layered system, and optional code on demand.
  • Most “REST APIs” skip full HATEOAS. That’s a reasonable choice, but small hypermedia touches like Location and next links still pay off.
  • HTTP semantics are defined in RFC 9110. Learn to read a raw request and response: start line, headers, empty line, body.
  • Content-Type describes the body you’re sending; Accept says what you want back.
  • Good APIs are consistent, predictable, discoverable, and evolvable.
  • ASP.NET Core supports both Minimal APIs and controllers. The design principles are the same for both; this handbook uses Minimal APIs.

Further Reading (Primary Sources)

More chapters are on the way

You have reached the end of the published chapters. These are coming next:

  1. 2 Resource Modelling and URL Design
  2. 3 HTTP Methods, Status Codes, and Semantics Done Right
  3. 4 Request/Response Design and Error Handling with RFC 9457 Problem Details
  4. 5 Pagination, Filtering, Sorting, and Searching
  5. 6 Contract-First Design with OpenAPI
  6. 7 API Versioning and Evolution Without Breaking Clients
  7. 8 Authentication and Authorization: API Keys, OAuth 2.0, OIDC, and JWT
  8. 9 API Security in Practice: The OWASP API Security Top 10 and Rate Limiting
  9. 10 Idempotency, Retries, and Resilient Third-Party Integrations (Polly and Circuit Breakers)
  10. 11 Webhooks and Asynchronous, Event-Driven Integration
  11. 12 Testing, Observability, and Choosing Beyond REST: Contract Tests, OpenTelemetry, GraphQL, and gRPC

Back to the series page