Almost every "REST vs gRPC" debate we get pulled into is really two different questions wearing one costume. What should a public-facing, browser-friendly API look like? And what should two of your own services use to talk to each other as fast as possible? Minimal APIs and gRPC both live in .NET 10, and the honest answer is usually both, in the same solution, for different audiences.
- Minimal APIs are the right default for anything a browser, a partner, or a third party calls directly — JSON is debuggable, cacheable, and universally understood.
- gRPC earns its complexity for internal service-to-service calls where you control both ends and want strict contracts, binary framing, and streaming.
- Kestrel can serve both from the same ASP.NET Core host — you don't need two separate applications.
- Don't reach for gRPC just because it's faster on paper. The debugging and tooling tax is real, and most internal APIs aren't actually your bottleneck.
Minimal APIs: the default for anything public
For public-facing or browser-consumed APIs, Minimal APIs give you almost everything MVC controllers used to require boilerplate for — route groups, typed results, model binding, and native OpenAPI generation — with far less ceremony.
var orders = app.MapGroup("/api/orders").WithTags("Orders");
orders.MapGet("/{id:guid}", async (Guid id, IOrderService svc, CancellationToken ct) =>
{
var order = await svc.GetAsync(id, ct);
return order is not null ? Results.Ok(order) : Results.NotFound();
})
.WithName("GetOrder")
.WithOpenApi();
orders.MapPost("/", async (CreateOrderRequest req, IOrderService svc, CancellationToken ct) =>
{
var order = await svc.CreateAsync(req, ct);
return Results.Created($"/api/orders/{order.Id}", order);
})
.WithName("CreateOrder")
.WithOpenApi();
It's JSON over HTTP/1.1, so any client can call it, browser tools can inspect it, and every observability platform already understands it out of the box.
gRPC: for calls between services you control
gRPC earns its keep when both ends of the call are your own services. You get a strongly-typed contract shared via .proto files, HTTP/2 multiplexing, binary framing that's meaningfully smaller and faster to (de)serialize than JSON, and native support for streaming.
syntax = "proto3";
option csharp_namespace = "Orders.Grpc";
service OrderTracker {
rpc GetStatus (OrderStatusRequest) returns (OrderStatusReply);
rpc StreamStatusUpdates (OrderStatusRequest) returns (stream OrderStatusReply);
}
message OrderStatusRequest {
string order_id = 1;
}
message OrderStatusReply {
string status = 1;
string updated_at = 2;
}
The service implementation is plain C# — no manual wire-format handling:
public class OrderTrackerService(IOrderStatusStore store) : OrderTracker.OrderTrackerBase
{
public override async Task GetStatus(
OrderStatusRequest request, ServerCallContext context)
{
var status = await store.GetAsync(request.OrderId, context.CancellationToken);
return new OrderStatusReply { Status = status.Value, UpdatedAt = status.UpdatedAt.ToString("O") };
}
public override async Task StreamStatusUpdates(
OrderStatusRequest request,
IServerStreamWriter responseStream,
ServerCallContext context)
{
await foreach (var update in store.WatchAsync(request.OrderId, context.CancellationToken))
{
await responseStream.WriteAsync(new OrderStatusReply
{
Status = update.Value,
UpdatedAt = update.UpdatedAt.ToString("O")
});
}
}
}
Server streaming is the feature Minimal APIs simply don't have a clean equivalent for — here a client can hold one call open and receive status updates as they happen, instead of polling.
Running both from the same host
You don't need two applications. Register both in the same Program.cs — Kestrel negotiates HTTP/1.1 for the JSON endpoints and HTTP/2 for gRPC over the same or separate ports.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
builder.Services.AddOpenApi();
builder.Services.AddScoped();
var app = builder.Build();
app.MapGrpcService();
var orders = app.MapGroup("/api/orders");
orders.MapGet("/{id:guid}", (Guid id, IOrderService svc) => svc.GetAsync(id));
app.MapOpenApi();
app.Run();
Picking one
| Minimal API (JSON) | gRPC | |
|---|---|---|
| Best for | Public/browser/partner clients | Internal service-to-service calls |
| Contract | OpenAPI, loosely typed | .proto, strongly typed |
| Streaming | Limited (SSE, WebSockets bolted on) | Native, in both directions |
| Debuggability | curl, browser devtools | Needs a gRPC-aware client |
| Payload size | Larger (JSON text) | Smaller (binary protobuf) |