.NET gRPC gRPC-Web TypeScript Protobuf APIs Performance

gRPC-Web no .NET: comunicação tipada entre browser e backend sem REST

Guia prático de gRPC-Web no .NET: diferença entre gRPC e gRPC-Web, configuração no ASP.NET Core, geração de cliente TypeScript com protoc.

N
Neryx Digital Architects
21 de novembro de 2025
12 min de leitura
220 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.