O gRPC é a escolha técnica superior para comunicação entre microsserviços: contrato forte com Protobuf, performance melhor que REST/JSON, streaming bidirecional, e cliente gerado automaticamente para qualquer linguagem. O problema: browsers não suportam HTTP/2 framing nativo que o gRPC requer. O gRPC-Web resolve isso — é um protocolo que adapta o gRPC para funcionar sobre HTTP/1.1 e HTTP/2 padrão do browser.
O resultado: você escreve seu serviço .NET uma vez com Protobuf, e tanto seus microsserviços internos quanto o frontend TypeScript consomem a mesma API tipada — sem duplicar modelos, sem manter duas versões de contrato.
gRPC vs gRPC-Web: a diferença técnica
O gRPC nativo usa HTTP/2 com framing de dados específico que os browsers bloqueiam via CORS por razões de segurança. O gRPC-Web é uma camada de compatibilidade que encapsula o payload gRPC em um envelope HTTP que os browsers conseguem processar. O cliente JavaScript/TypeScript do gRPC-Web envia requisições HTTP normais com headers especiais, e um proxy (ou o próprio ASP.NET Core) converte para o protocolo gRPC nativo no servidor.
Limitação principal do gRPC-Web em relação ao gRPC nativo: não suporta streaming bidirecional nem streaming do cliente (client-streaming). Suporta streaming do servidor (server-streaming), que é o caso de uso mais comum para interfaces web.
Configuração no ASP.NET Core
dotnet add package Grpc.AspNetCore
dotnet add package Grpc.AspNetCore.Web
// Protos/orders.proto
syntax = "proto3";
option csharp_namespace = "MyApp.Grpc";
package orders;
service OrderService {
// Unary — uma requisição, uma resposta
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
// Server streaming — uma requisição, stream de respostas
// Útil para atualizações em tempo real no browser
rpc WatchOrderStatus (WatchOrderStatusRequest)
returns (stream OrderStatusUpdate);
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse);
}
message GetOrderRequest {
string order_id = 1;
}
message OrderResponse {
string id = 1;
string customer_name = 2;
double total = 3;
string status = 4;
string created_at = 5;
repeated OrderItemResponse items = 6;
}
message OrderItemResponse {
string product_id = 1;
string product_name = 2;
int32 quantity = 3;
double unit_price = 4;
}
message WatchOrderStatusRequest {
string order_id = 1;
}
message OrderStatusUpdate {
string order_id = 1;
string status = 2;
string updated_at = 3;
string message = 4;
}
message CreateOrderRequest {
string customer_id = 1;
repeated CreateOrderItem items = 2;
}
message CreateOrderItem {
string product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
message CreateOrderResponse {
string order_id = 1;
double total = 2;
}
message ListOrdersRequest {
int32 page = 1;
int32 page_size = 2;
string status_filter = 3;
}
message ListOrdersResponse {
repeated OrderResponse orders = 1;
int32 total_count = 2;
}
<!-- MyApp.csproj — geração automática de código C# a partir do .proto -->
<ItemGroup>
<Protobuf Include="Protos\orders.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.*" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.*" />
</ItemGroup>
// Program.cs — habilita gRPC e gRPC-Web
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();
// UseGrpcWeb DEVE vir antes de UseRouting
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
// CORS para o browser — ajuste as origens para produção
app.UseCors(policy => policy
.WithOrigins("http://localhost:3000", "https://app.seusite.com.br")
.AllowAnyHeader()
.AllowAnyMethod()
// Headers específicos do gRPC-Web que precisam ser expostos
.WithExposedHeaders(
"Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding"));
app.MapGrpcService<OrderGrpcService>().EnableGrpcWeb().RequireCors("grpc-web");
app.Run();
Implementação do serviço .NET
// Services/OrderGrpcService.cs
public class OrderGrpcService : OrderService.OrderServiceBase
{
private readonly IOrderRepository _repository;
private readonly IOrderEventStream _eventStream;
public OrderGrpcService(IOrderRepository repository, IOrderEventStream eventStream)
{
_repository = repository;
_eventStream = eventStream;
}
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request,
ServerCallContext context)
{
var order = await _repository.GetByIdAsync(
Guid.Parse(request.OrderId),
context.CancellationToken);
if (order is null)
{
// Erros gRPC usam status codes próprios
throw new RpcException(new Status(
StatusCode.NotFound,
$"Pedido {request.OrderId} não encontrado"));
}
return MapToResponse(order);
}
// Server streaming: o cliente recebe updates em tempo real
public override async Task WatchOrderStatus(
WatchOrderStatusRequest request,
IServerStreamWriter<OrderStatusUpdate> responseStream,
ServerCallContext context)
{
var orderId = Guid.Parse(request.OrderId);
// Envia o status atual imediatamente
var current = await _repository.GetStatusAsync(orderId, context.CancellationToken);
await responseStream.WriteAsync(new OrderStatusUpdate
{
OrderId = request.OrderId,
Status = current.Status,
UpdatedAt = current.UpdatedAt.ToString("O"),
Message = "Status atual"
});
// Fica ouvindo eventos de atualização até o cliente cancelar
await foreach (var update in _eventStream
.WatchOrderAsync(orderId, context.CancellationToken))
{
if (context.CancellationToken.IsCancellationRequested)
break;
await responseStream.WriteAsync(new OrderStatusUpdate
{
OrderId = request.OrderId,
Status = update.NewStatus,
UpdatedAt = update.Timestamp.ToString("O"),
Message = update.Description
});
}
}
private static OrderResponse MapToResponse(Order order) => new()
{
Id = order.Id.ToString(),
CustomerName = order.CustomerName,
Total = (double)order.Total,
Status = order.Status.ToString(),
CreatedAt = order.CreatedAt.ToString("O"),
Items = { order.Items.Select(i => new OrderItemResponse
{
ProductId = i.ProductId.ToString(),
ProductName = i.ProductName,
Quantity = i.Quantity,
UnitPrice = (double)i.UnitPrice
})}
};
}
Cliente TypeScript gerado automaticamente
# Instala o gerador de código TypeScript para gRPC-Web
npm install -D grpc-tools grpc_tools_node_protoc_ts
# Ou via protoc plugin oficial
npm install -D @protobuf-ts/plugin @protobuf-ts/grpcweb-transport
# Gera o cliente TypeScript a partir do .proto
npx protoc \
--plugin=protoc-gen-ts=$(which protoc-gen-ts) \
--ts_out=./src/generated \
--ts_opt=generate_dependencies \
-I ../MyApp/Protos \
orders.proto
// src/services/orderService.ts — cliente TypeScript tipado, gerado automaticamente
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
import { OrderServiceClient } from '../generated/orders.client';
const transport = new GrpcWebFetchTransport({
baseUrl: 'http://localhost:5000',
});
const client = new OrderServiceClient(transport);
// Chamada unary — completamente tipada
export async function getOrder(orderId: string) {
const { response } = await client.getOrder({ orderId });
// response é tipado como OrderResponse — sem casting manual
return response;
}
// Streaming server-side — atualizações em tempo real
export async function watchOrderStatus(
orderId: string,
onUpdate: (update: OrderStatusUpdate) => void
) {
const stream = client.watchOrderStatus({ orderId });
for await (const update of stream.responses) {
// update é tipado como OrderStatusUpdate
onUpdate(update);
}
}
// Uso em um componente React:
// useEffect(() => {
// const abort = new AbortController();
// watchOrderStatus(orderId, setOrderStatus);
// return () => abort.abort();
// }, [orderId]);
Autenticação JWT com gRPC-Web
// Passando JWT nos metadados da requisição gRPC-Web
const transport = new GrpcWebFetchTransport({
baseUrl: 'http://localhost:5000',
interceptors: [
{
interceptUnary(next, method, input, options) {
const token = localStorage.getItem('jwt_token');
if (token) {
options.meta = {
...options.meta,
'Authorization': `Bearer ${token}`
};
}
return next(method, input, options);
}
}
]
});
// No servidor .NET — leitura do JWT como em qualquer API
// Os metadados gRPC chegam como headers HTTP, então JWT auth funciona normalmente
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.Authority = builder.Configuration["Auth:Authority"];
opt.Audience = builder.Configuration["Auth:Audience"];
});
// Proteger endpoints gRPC com [Authorize] normalmente:
[Authorize]
public override async Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
// context.GetHttpContext().User — usuário autenticado disponível
var userId = context.GetHttpContext().User.FindFirst("sub")?.Value;
// ...
}
Quando usar gRPC-Web vs REST vs WebSockets
| Critério | gRPC-Web | REST/JSON | WebSockets |
|---|---|---|---|
| Contrato de API | ✅ Forte (Protobuf) | ⚠️ OpenAPI (opcional) | ❌ Manual |
| Cliente gerado | ✅ Automático | ⚠️ OpenAPI Codegen | ❌ Manual |
| Streaming servidor→browser | ✅ Sim | ⚠️ SSE (limitado) | ✅ Sim |
| Streaming bidirecional | ❌ Não | ❌ Não | ✅ Sim |
| Tamanho do payload | ✅ Menor (binário) | ⚠️ Maior (JSON) | ⚠️ Depende |
| Debugging | ⚠️ Requer Protobuf decode | ✅ Fácil (texto) | ⚠️ Médio |
| Adoção no mercado | ⚠️ Crescendo | ✅ Universal | ✅ Ampla |
gRPC-Web é a escolha certa quando você já tem serviços gRPC internos e quer reutilizar os contratos Protobuf no frontend, quando o payload de dados é alto (mobile com banda limitada, payloads grandes) e a eficiência binária importa, ou quando a consistência de tipos entre backend e frontend é prioritária. Para a maioria das aplicações web CRUD convencionais, REST/JSON ainda é a escolha mais simples e com melhor tooling.
A escolha entre gRPC-Web, REST e WebSockets depende do seu contexto. Se você quer desenhar a estratégia de API do seu produto com o stack certo para cada caso, a Neryx pode ajudar.