<!-- Canonical: https://lucioduran.com/es/blog/grpc-protobuf-connectrpc-api-design
Language: es
Content-SHA256: 1ef480afc91294cd8706130bd243175e3b1b2534b9c2de958db2a8c7fceb911a
-->

API Design

# gRPC + Protobuf + ConnectRPC: APIs Type-Safe con Streaming y Generación de Código

2026-01-067 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/grpc-protobuf-connectrpc-api-design>)

## Protocol Buffers: La Fuente de Verdad de Tu API

El contrato de API reemplaza al YAML de OpenAPI y a los editores Swagger. Tu contrato de API es un archivo ` .proto `, y todo lo demás se genera de ahí.

```protobuf
// proto/blog/v1/blog.proto
syntax = "proto3";

package blog.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";
import "buf/validate/validate.proto";

message Post {
 string id = 1;
 string title = 2 [(buf.validate.field).string.min_len = 1];
 string content = 3;
 string author_id = 4;
 repeated string tags = 5;
 PostStatus status = 6;
 google.protobuf.Timestamp created_at = 7;
 google.protobuf.Timestamp updated_at = 8;
 optional string featured_image_url = 9;
}

enum PostStatus {
 POST_STATUS_UNSPECIFIED = 0;
 POST_STATUS_DRAFT = 1;
 POST_STATUS_PUBLISHED = 2;
 POST_STATUS_ARCHIVED = 3;
}

message CreatePostRequest {
 string title = 1 [(buf.validate.field).string = {min_len: 1, max_len: 200}];
 string content = 2 [(buf.validate.field).string.min_len = 1];
 repeated string tags = 3 [(buf.validate.field).repeated.max_items = 10];
 optional string featured_image_url = 4;
}

message CreatePostResponse {
 Post post = 1;
}

message ListPostsRequest {
 int32 page_size = 1 [(buf.validate.field).int32 = {gte: 1, lte: 100}];
 string page_token = 2;
 optional PostStatus status_filter = 3;
}

message ListPostsResponse {
 repeated Post posts = 1;
 string next_page_token = 2;
 int32 total_count = 3;
}

message StreamPostsRequest {
 repeated string author_ids = 1;
 repeated PostStatus status_filter = 2;
}

service BlogService {
 rpc CreatePost(CreatePostRequest) returns (CreatePostResponse);
 rpc ListPosts(ListPostsRequest) returns (ListPostsResponse);
 rpc StreamPosts(StreamPostsRequest) returns (stream Post);
}
```

Observar en algunas cosas. El keyword ` optional ` hace trabajo real aquí, en proto3, todos los campos tienen defaults de zero-value. Un ` string ` defaultea a ` "" `, un ` int32 ` a ` 0 `. Sin ` optional `, no es posible saber si el cliente mandó un string vacío o no mandó el campo directamente. Para operaciones de update, esa distinción importa muchísimo.

El ` FieldMask ` en el request de update es la forma proto de hacer updates parciales. En vez del debate REST sobre semántica de PATCH, el cliente dice explícitamente "estoy actualizando estos campos" y el servidor lo respeta.

## Generación de Código: Automatización del Contrato

Usando ` buf ` (el toolchain moderno de protobuf):

```yaml
# buf.gen.yaml
version: v2
plugins:
 - remote: buf.build/protocolbuffers/go
 out: gen/go
 opt: paths=source_relative
 - remote: buf.build/connectrpc/go
 out: gen/go
 opt: paths=source_relative
 - remote: buf.build/connectrpc/es
 out: gen/ts
 opt: target=ts
 - remote: buf.build/bufbuild/es
 out: gen/ts
 opt: target=ts
```

```bash
buf generate
```

Eso genera interfaces tipadas de servidor en Go, stubs de cliente en Go, stubs de cliente en TypeScript, y helpers de validación. De un solo archivo ` .proto `. Cuando el proto cambia, regenerar muestra cada ubicación que se rompió en tiempo de compilación. Difícil lograr eso con una spec OpenAPI.

## El Servidor Go: ConnectRPC en la Práctica

```go
type BlogServer struct {
 db *Database
}

func (s *BlogServer) CreatePost(
 ctx context.Context,
 req *connect.Request[blogv1.CreatePostRequest],
) (*connect.Response[blogv1.CreatePostResponse], error) {
 post := &blogv1.Post{
 Id: generateID(),
 Title: req.Msg.Title,
 Content: req.Msg.Content,
 AuthorId: getUserID(ctx),
 Tags: req.Msg.Tags,
 Status: blogv1.PostStatus_POST_STATUS_DRAFT,
 CreatedAt: timestamppb.Now(),
 }

 if err := s.db.Posts.Create(ctx, post); err != nil {
 return nil, connect.NewError(connect.CodeInternal, err)
 }

 return connect.NewResponse(&blogv1.CreatePostResponse{
 Post: post,
 }), nil
}

func (s *BlogServer) StreamPosts(
 ctx context.Context,
 req *connect.Request[blogv1.StreamPostsRequest],
 stream *connect.ServerStream[blogv1.Post],
) error {
 ch, cleanup := s.db.Posts.Subscribe(ctx, req.Msg.AuthorIds)
 defer cleanup()

 ticker := time.NewTicker(30 * time.Second)
 defer ticker.Stop()

 for {
 select {
 case <-ctx.Done():
 return nil
 case post, ok := <-ch:
 if !ok {
 return nil
 }
 if err := stream.Send(post); err != nil {
 return err
 }
 case <-ticker.C:
 // Keepalive para que el LB no corte el stream
 }
 }
}
```

[ConnectRPC](<https://connectrpc.com/>) es simplemente un handler HTTP. Se monta en el mux estándar de ` net/http `. Puede montarlo junto a tus endpoints REST, tus health checks, tu endpoint de métricas. Esa componibilidad es una ventaja importante sobre gRPC-Go crudo, que quiere adueñarse de todo el ciclo de vida del servidor.

## El Cliente TypeScript: Browser + Node.js

```typescript
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { BlogService } from "./gen/ts/blog/v1/blog_pb";

const transport = createConnectTransport({
 baseUrl: "https://api.miapp.com",
});

const client = createClient(BlogService, transport);

// Llamada unaria, completamente tipada
async function createPost(title: string, content: string) {
 const response = await client.createPost({
 title,
 content,
 tags: ["engineering"],
 });
 console.log("Creado:", response.post?.id);
 return response.post;
}

// Server streaming, feed en tiempo real
async function watchPosts(authorIds: string[]) {
 for await (const post of client.streamPosts({ authorIds })) {
 console.log("Post nuevo:", post.title);
 updateUI(post);
 }
}
```

Todo los campos están tipados. Todos los métodos están tipados. La llamada a ` createPost ` no compila si pasás un número como ` title ` o te olvidás un campo requerido.

## La Comparación Honesta: gRPC vs REST vs tRPC

Basándose en experiencia de producción con los tres, la verdad sin barniz:

**Type safety a través del cable**: gRPC/ConnectRPC gana. El contrato se define una vez en ` .proto ` y el código generado está garantizado de matchear. REST con codegen de OpenAPI se acerca pero las specs se desincronan. tRPC tiene excelente type safety pero solo dentro de TypeScript.

**Performance**: Protobuf binario es 3-10x más pequeño que JSON para payloads típicos. En calls server-to-server de alto throughput, esto importa. Para un browser cargando una sola página, la diferencia es negligible.

**Streaming**: gRPC tiene streaming de primera clase. REST requiere WebSockets o SSE, que son protocolos atornillados. tRPC soporta subscriptions vía WebSockets pero se siente como un afterthought.

**Ecosistema**: REST gana abrumadoramente. Toda herramienta, todo proxy, todo sistema de monitoreo entiende HTTP/JSON. gRPC requiere infraestructura aware de HTTP/2. ConnectRPC puentea esta brecha soportando ambos protocolos.

## Load Balancing: El Gotcha que Destroza Deploys

gRPC usa HTTP/2, que multiplexa muchos requests sobre una sola conexión TCP. Genial para performance pero terrible para load balancing.

Un load balancer L4 estándar (como [AWS](<https://aws.com/>) NLB) balancea a nivel de conexión. Si tu cliente gRPC abre una conexión al load balancer, todos los requests van al mismo backend. Tu servicio "balanceado" tiene todo el tráfico pegándole a un pod.

Soluciones:

**Load balancing L7**: Utilizar un proxy L7 que entienda frames HTTP/2, Envoy, nginx (con módulo gRPC), o LBs cloud-native como AWS ALB o el LB gRPC-aware de GCP.

**Balanceo client-side**: El cliente gRPC mantiene conexiones a múltiples backends y distribuye requests. Este es el approach estándar en Kubernetes con servicios headless.

**Service mesh**: Si se está en Kubernetes, Linkerd o Istio manejan el load balancing de gRPC transparentemente.

## Cuándo Usar Este Stack

gRPC + ConnectRPC es la opción correcta cuando:

- Se construye comunicación service-to-service en un sistema polyglot
- Se necesita streaming (feeds real-time, operaciones de larga duración)
- La superficie de API está bien definida y no va a cambiar de forma frecuentemente
- Quiero un solo contrato que genere clientes para web, mobile y server

REST sigue siendo mejor opción cuando:

- La API es pública y necesita máxima compatibilidad
- El equipo es pequeño y no quiere aprender protobuf
- La API es heavy en CRUD con patrones simples de request/response

tRPC encaja mejor cuando:

- Todo el stack es TypeScript (monorepo Next.js)
- La velocidad de desarrollo importa más que el soporte cross-language
- La API es interna y el equipo es pequeño

El tooling maduró lo suficiente como para que el argumento de "gRPC es demasiado complejo" ya no se sostenga. ` buf ` hace que el manejo de protos sea tan fácil como ` npm `. ConnectRPC hace que los clientes browser sean triviales. El payoff de type safety y code generation es real y se compone con el tiempo.

Comenzar con un archivo ` .proto `. Todo lo demás sigue de ahí.

## Preguntas frecuentes

### ¿Debería usar gRPC o REST para una API nueva?

Si tus clientes son todos server-side (microservicios, herramientas internas), gRPC gana en type safety, performance y code generation. Si se necesita clientes browser, ConnectRPC te da la experiencia de desarrollo de gRPC con fallback a HTTP/JSON. Las APIs REST públicas todavía tienen sentido cuando se necesita máxima compatibilidad de ecosistema.

### ¿Cómo se compara ConnectRPC con tRPC?

tRPC es solo TypeScript y está fuertemente acoplado al ecosistema TS. ConnectRPC genera clientes para Go, TypeScript, Java, Swift, Kotlin y más desde un solo archivo .proto. Si se es polyglot o se necesita clientes mobile, ConnectRPC gana. Si se es todo TypeScript en un monorepo, tRPC es más simple.

### ¿Y GraphQL?

GraphQL resuelve un problema diferente, queries flexibles dirigidas por el cliente. gRPC resuelve RPC con contratos fuertes. Si tus clientes necesitan consultar combinaciones arbitrarias de campos entre entidades, GraphQL. Si teners operaciones bien definidas, gRPC/ConnectRPC.

## Tips

1. **Tip:** Siempre utilizar campos ` optional ` en proto3 en vez de confiar en defaults de zero-value. Sin ` optional `, no es posible distinguir entre 'el campo se seteó a 0' y 'el campo no se mandó', esto causa problemas fuerte en updates estilo PATCH.
2. **Tip:** Habilitar gRPC reflection en todos los entornos que no sean producción. Herramientas como ` grpcurl ` y ` grpcui ` son inútiles sin ella, y tu equipo va a perder horas adivinando definiciones de servicio.
3. **Tip:** ConnectRPC soporta tanto gRPC como JSON sobre HTTP, utilizar el modo JSON para clientes browser y debugging mobile, y protobuf binario para server-to-server. Misma definición de servicio, cero cambios de código.
4. **Tip:** Cuando uses streaming RPCs, siempre implementá un ` keepalive ` ping. Los load balancers (especialmente AWS ALB) terminan silenciosamente streams HTTP/2 idle después de 60 segundos por defecto.

- grpc
- protobuf
- connectrpc
- api-design
- streaming
- rpc
- typescript
- golang

[Volver al Blog](<https://lucioduran.com/es/blog>)
