API Styles in .NET: REST vs gRPC vs GraphQL
How to pick between REST, gRPC, and GraphQL in .NET, when a BFF helps, and what each style actually costs in latency and developer time.
Table of contents
- When does the API style actually matter?
- What numbers should I budget for the choice?
- What does the minimal architecture look like?
- What does the .NET 10 wiring look like?
- How do I avoid the classic API-style mistakes?
- What failure modes does each style introduce?
- When should you skip the API-style debate?
- Where should you go from here?
The API style choice gets decided in the first design meeting and lives in every line of client code afterward. Pick wrong and the team will work around it for years. This chapter breaks the choice into the three that matter for .NET - REST, gRPC, GraphQL - plus the BFF pattern that resolves a specific aggregation pain.
When does the API style actually matter?
Three signals.
Many clients, different shapes. If web wants the user's display name and mobile wants the same plus profile photo and partner integrations want a stripped down ID-only view, REST endpoints multiply. GraphQL collapses them.
Tight internal latency budget. Service A calls Service B 50 times to render a request. Each call adds JSON parsing and HTTP overhead. gRPC's binary Protobuf cuts the per-call cost ~3-5x and HTTP/2 multiplexes them on one connection.
External developer experience matters. A public API for partner integrations is read by humans with curl. JSON wins; Protobuf is opaque without tooling.
If none of these dominate, REST + JSON is the right default. The remaining 80% of .NET services never need anything else.
What numbers should I budget for the choice?
Style Payload size Encode/decode Browser-friendly
REST+JSON 1x baseline ~50 µs yes
gRPC ~0.3-0.6x ~10-20 µs no (needs grpc-web)
GraphQL varies (custom) ~50-100 µs yes
For per-call latency, the network dominates everything for any hop > 1 ms. Switching from REST to gRPC saves ~30 µs of CPU; if your service is across the internet at 50 ms RTT, that is invisible. gRPC's win shows up in the data centre at <1 ms RTT, where the CPU saving matters.
What does the minimal architecture look like?
flowchart LR
Web[Web client] -->|REST+JSON| Edge[ASP.NET Core API]
Mobile[Mobile client] -->|REST+JSON| Edge
Edge -->|gRPC| SvcA[Inventory service]
Edge -->|gRPC| SvcB[Pricing service]
Edge -->|gRPC| SvcC[User service]
Edge --> DB[(Postgres)]
External clients speak REST+JSON; internal services speak gRPC. The edge service translates - one REST endpoint backed by three internal gRPC calls. This is the most common shape in mature .NET microservice stacks and it gives you both ergonomics where it matters.
What does the .NET 10 wiring look like?
REST is built into ASP.NET Core - no setup. gRPC needs a .proto
file plus the package:
// inventory.proto - shared contract
service Inventory {
rpc GetStock (StockRequest) returns (StockReply);
}
message StockRequest { string sku = 1; }
message StockReply { int32 quantity = 1; bool available = 2; }
// Server side
builder.Services.AddGrpc();
app.MapGrpcService<InventoryGrpcService>();
public class InventoryGrpcService(IStockRepo repo) : Inventory.InventoryBase
{
public override async Task<StockReply> GetStock(StockRequest req, ServerCallContext ctx)
{
var qty = await repo.GetQuantityAsync(req.Sku, ctx.CancellationToken);
return new StockReply { Quantity = qty, Available = qty > 0 };
}
}
// Client side - generated client, used like a normal service
builder.Services.AddGrpcClient<Inventory.InventoryClient>(o =>
{
o.Address = new Uri("https://inventory:5001");
});
public class CheckoutService(Inventory.InventoryClient inv)
{
public async Task<bool> CanFulfill(string sku)
{
var reply = await inv.GetStockAsync(new StockRequest { Sku = sku });
return reply.Available;
}
}
GraphQL with Hot Chocolate looks similar - schema-first or code-first, mapped to your domain types, with built-in DataLoader to solve the N+1 problem. The full example fits in any Hot Chocolate quickstart and is too long to inline here.
How do I avoid the classic API-style mistakes?
Four mistakes I keep seeing in code reviews:
- REST verbs lying -
POST /searchis fine;POST /getUseris not. Either use proper HTTP verbs or use gRPC. Mixing the two loses HTTP caching and confuses clients. - gRPC across the internet - Protobuf payload is small but the HTTP/2 connection overhead is real, and many corporate proxies break gRPC. Inside the data centre, fine. Across the internet to partners, REST.
- GraphQL without DataLoader - the N+1 query problem is the default in GraphQL; one query for posts plus one query per post for the author. DataLoader batches them. Forgetting it kills the database.
- One BFF for many clients - the whole point of a BFF is to customise per client. Sharing one BFF across iOS, Android, and Web turns it into a bloated middle layer with the worst of all worlds.
What failure modes does each style introduce?
- REST - schema drift between client and server (no contract);
N+1 calls when the resource is graph-shaped. Mitigation: OpenAPI
- generated clients; design endpoints for screens, not entities.
- gRPC - opaque payloads make production debugging harder; proxy compatibility issues; harder to test with curl. Mitigation: log serialised messages at the edge, expose a REST gateway for ad-hoc tools.
- GraphQL - query complexity attacks (one
nested(depth:50)query melts the server); cache-busting variability (each query string is unique). Mitigation: Hot Chocolate's complexity limits- persisted queries.
Chapter 13 instruments all three uniformly through OpenTelemetry's HTTP/gRPC support.
When should you skip the API-style debate?
When traffic is low and the team is small. A single REST API on ASP.NET Core handles 10K QPS comfortably. The gRPC win matters at service mesh scale; the GraphQL win matters at multi-platform scale. A startup with one API and one mobile app gets nothing from either beyond complexity. Stay with REST + JSON until the QPS estimate or the multi-client pain forces a real change.
Where should you go from here?
Next chapter: Elasticsearch in .NET apps - when LIKE queries on Postgres run out of road and you need a real search engine. After that, auth styles closes out the building-blocks group.