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.

Key takeaways
  • 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.

csharp
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.

OrderTracker.proto
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:

csharp
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.

csharp
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 forPublic/browser/partner clientsInternal service-to-service calls
ContractOpenAPI, loosely typed.proto, strongly typed
StreamingLimited (SSE, WebSockets bolted on)Native, in both directions
Debuggabilitycurl, browser devtoolsNeeds a gRPC-aware client
Payload sizeLarger (JSON text)Smaller (binary protobuf)
Our default adviceIf you're not sure yet, start with Minimal APIs everywhere. Add gRPC to a specific internal call path only once you've measured that JSON serialization or HTTP overhead is actually the bottleneck — not before.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.