🔌 APIs REST y gRPC en Go
Diseño de APIs REST profesionales (handlers, middlewares JWT, validación) y gRPC completo: protobuf, servidores, clientes, streaming e interceptors. Con ejemplos funcionales.
APIs REST y gRPC en Go
Exponer tu lógica al mundo (o entre servicios) requiere elegir cómo comunicarse. Dos estilos dominan: REST (JSON sobre HTTP, humano y universal) y gRPC (binario, tipado y rapidísimo sobre HTTP/2). En Go dominas ambos con el mismo lenguaje. Este artículo te enseña a construir APIs REST profesionales y servicios gRPC completos, y a decidir cuándo usar cada uno.
API REST en Go: la arquitectura
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type Task struct {
ID int `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
}
type Store struct {
tasks map[int]Task
next int
}
func (s *Store) Create(t Task) Task {
t.ID = s.next
s.next++
s.tasks[t.ID] = t
return t
}
func (s *Store) Get(id int) (Task, bool) {
t, ok := s.tasks[id]
return t, ok
}
func main() {
store := &Store{tasks: map[int]Task{}, next: 1}
mux := http.NewServeMux()
mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
var t Task
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
http.Error(w, `{"error":"body inválido"}`, http.StatusBadRequest)
return
}
created := store.Create(t)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(created)
})
mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
var n int
if _, err := fmt.Sscanf(r.PathValue("id"), "%d", &n); err != nil {
http.Error(w, `{"error":"id inválido"}`, http.StatusBadRequest)
return
}
t, ok := store.Get(n)
if !ok {
http.Error(w, `{"error":"no encontrado"}`, http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(t)
})
log.Println("API en :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
💡 Una API profesional se separa en capas: routing, handlers (HTTP ↔ lógica), middlewares (auth, logging, CORS) y servicio/repositorio. El estándar moderno es
net/http+http.ServeMuxcon el patrón de método en la ruta (Go 1.22+):mux.HandleFunc("POST /tasks", …)registra método y ruta juntos,{id}captura parámetros que lees conr.PathValue, y un método incorrecto devuelve405automáticamente.
Handlers: structs con dependencias
Un handler no debe ser una closure gigante ni depender de globales. Usa una struct que inyecta las dependencias (store, logger, etc.):
type TaskAPI struct {
store *Store
}
func (a *TaskAPI) Create(w http.ResponseWriter, r *http.Request) {
var t Task
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
http.Error(w, `{"error":"body inválido"}`, http.StatusBadRequest)
return
}
if t.Text == "" {
http.Error(w, `{"error":"text requerido"}`, http.StatusUnprocessableEntity)
return
}
writeJSON(w, http.StatusCreated, a.store.Create(t))
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
Registra los métodos del struct en el mux: mux.HandleFunc("POST /tasks", api.Create). ⚠️ Los handlers deben ser métodos sobre un struct, no closures: el struct inyecta dependencias (testeable con un store fake) y evita globales mutables que rompen la concurrencia.
Middlewares: auth JWT y logging
Un middleware envuelve un handler y añade lógica antes/después. Se componen en cadena. El patrón: una función que recibe un http.Handler y devuelve otro. Un ejemplo de logging: func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path); next.ServeHTTP(w, r) }) }.
Middleware de autenticación JWT
Valida un token JWT en la cabecera Authorization: Bearer <token>:
import (
"fmt"
"strings"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("cambia-esto-en-produccion")
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
tokenStr, ok := strings.CutPrefix(auth, "Bearer ")
if !ok {
http.Error(w, `{"error":"token requerido"}`, http.StatusUnauthorized)
return
}
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("método inesperado: %v", t.Header["alg"])
}
return jwtSecret, nil
})
if err != nil || !token.Valid {
http.Error(w, `{"error":"token inválido"}`, http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Para emitir tokens en el login: jwt.NewWithClaims(jwt.SigningMethodHS256,
// claims).SignedString(jwtSecret) con claims como {"sub": usuarioID, "exp": expiración}.
Componer los middlewares alrededor del mux con protected := authMiddleware(loggingMiddleware(mux)) y servir protected en vez del mux plano.
⚠️ Nunca valides un JWT sin comprobar el algoritmo (el ataque clásico es un token firmado con
noneo con un algoritmo equivocado). La comprobaciónif _, ok := t.Method.(*jwt.SigningMethodHMAC); !okdentro del keyfunc dejwt.Parseevita exactamente eso.
Validación de entrada
La validación separa «el dato está bien formado» de «la lógica falló». Un enfoque simple y legible es manual; para schemas ricos usa go-playground/validator:
import "github.com/go-playground/validator/v10"
var validate = validator.New()
type CreateTaskRequest struct {
Text string `json:"text" validate:"required,min=3,max=200"`
Due string `json:"due" validate:"omitempty,datetime=2006-01-02"`
}
func (a *TaskAPI) Create(w http.ResponseWriter, r *http.Request) {
var req CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"body inválido"}`, http.StatusBadRequest)
return
}
if err := validate.Struct(&req); err != nil {
http.Error(w, `{"error":"validación falló"}`, http.StatusUnprocessableEntity)
return
}
// …procede con req.Text y req.Due…
}
¿Qué es gRPC?
gRPC es un framework de RPC (Remote Procedure Call) de Google: llamas a un método remoto como si fuera local, con HTTP/2 binario y protobuf como serialización (altísimo rendimiento, contrato tipado, streaming nativo). La comparación con REST:
| Característica | REST | gRPC |
|---|---|---|
| Formato de datos | JSON (texto legible) | Protobuf (binario compacto) |
| Protocolo | HTTP/1.1 / HTTP/2 | HTTP/2 (multiplexado, binario) |
| Contrato | Informal (OpenAPI opcional) | Definido en .proto (fuerte) |
| Streaming | Duro (SSE/WebSocket) | Nativo (unary, server, client, bidi) |
| Rendimiento | Bueno | Muy superior |
| Depuración | Fácil (legible) | Más difícil (binario) |
Protobuf: el contrato .proto
Todo empieza con un archivo .proto (sintaxis proto3): define mensajes (structs) y servicios (métodos RPC). Los números de campo (= 1, = 2) son el identificador en el cable: no los renombres sin migrar, porque rompes la compatibilidad.
syntax = "proto3";
package task;
option go_package = "example.com/api/taskpb";
// Un mensaje es como una struct con campos numerados
message Task {
int32 id = 1;
string text = 2;
bool done = 3;
}
message GetTaskRequest { int32 id = 1; } // una línea por mensaje simple
message CreateTaskRequest { string text = 1; }
service TaskService {
rpc GetTask(GetTaskRequest) returns (Task);
rpc CreateTask(CreateTaskRequest) returns (Task);
}
Generación de código
protoc lee el .proto y, con los dos plugins obligatorios (protoc-gen-go genera los mensajes task.pb.go; protoc-gen-go-grpc genera las interfaces del servicio task_grpc.pb.go), genera código Go:
# instalar los dos plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# generar código desde task.proto
protoc --go_out=. --go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
task.proto
Como el código es generado, no lo edites a mano — edita el .proto y regenera.
Servidor gRPC
Implementas la interfaz generada sobre un grpc.Server:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
pb "example.com/api/taskpb"
)
type server struct {
pb.UnimplementedTaskServiceServer
tasks map[int32]*pb.Task
next int32
}
func (s *server) GetTask(ctx context.Context, req *pb.GetTaskRequest) (*pb.Task, error) {
t, ok := s.tasks[req.Id]
if !ok {
return nil, status.Error(codes.NotFound, "task no encontrada")
}
return t, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
grpcServer := grpc.NewServer()
pb.RegisterTaskServiceServer(grpcServer, &server{tasks: map[int32]*pb.Task{}, next: 1})
reflection.Register(grpcServer) // para grpcurl
log.Println("gRPC escuchando en :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatal(err)
}
}
Cliente gRPC
Con el stub generado, el cliente es casi trivial — conecta, crea el client y llama:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "example.com/api/taskpb"
)
func main() {
// grpc.NewClient (Go 1.63+; grpc.Dial está deprecado) no conecta de inmediato.
// En local, sin TLS: insecure. Nunca uses insecure en producción sin cifrado.
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := pb.NewTaskServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
created, err := client.CreateTask(ctx, &pb.CreateTaskRequest{Text: "aprender gRPC"})
if err != nil {
log.Fatal(err)
}
log.Printf("creada: id=%d text=%q", created.Id, created.Text)
}
Streaming: servidor, cliente y bidi
El streaming es la gran ventaja de gRPC sobre REST. Cuatro modos sobre el mismo concepto de flujos:
| Modo | Proto | Comportamiento |
|---|---|---|
| Server stream | returns (stream Task) |
servidor stream.Send en bucle; cliente Recv hasta io.EOF |
| Client stream | (stream Req) returns (Resp) |
cliente Send + CloseAndRecv; servidor Recv hasta io.EOF y cierra con SendAndClose |
| Bidi | (stream Msg) returns (stream Msg) |
ambos Recv/Send independientes |
Ejemplo de server streaming (el servidor envía varias respuestas; el proto es rpc ListTasks(ListTasksRequest) returns (stream Task)):
func (s *server) ListTasks(req *pb.ListTasksRequest, stream pb.TaskService_ListTasksServer) error {
for _, t := range s.tasks {
if err := stream.Send(t); err != nil {
return err // cliente se desconectó
}
}
return nil
}
Interceptors: auth y logging en gRPC
Los interceptors son los middlewares de gRPC. Se configuran en el grpc.Server y envuelven cada llamada o stream. ⚠️ Los metadatos viajan en el contexto gRPC (no en cabeceras HTTP): tokens y tracing se leen con metadata.FromIncomingContext(ctx).
Los interceptors son los middlewares de gRPC. Se configuran en el grpc.Server y envuelven cada llamada o stream:
// Interceptor de logging (firma completa de UnaryServerInterceptor):
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
log.Printf("llamada: %s", info.FullMethod)
return handler(ctx, req)
}
// Interceptor de autenticación:
func authInterceptor(validToken string) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok || len(md["authorization"]) == 0 || md["authorization"][0] != "Bearer "+validToken {
return nil, status.Error(codes.Unauthenticated, "token requerido")
}
return handler(ctx, req)
}
}
Regístralo al crear el server: grpcServer := grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor)).
Para varios interceptors se encadenan con grpc.ChainUnaryInterceptor(auth, logging).
REST vs gRPC: cuándo cada uno
| Situación | Elige |
|---|---|
| API pública para navegadores/web | REST |
| Clientes JavaScript / terceros | REST |
| Comunicación servicio↔servicio interno | gRPC |
| Alto throughput y baja latencia | gRPC |
| Streaming de datos en tiempo real | gRPC |
| Depuración y curva de aprendizaje | REST |
| Contrato estricto multi-lenguaje | gRPC |
💡 Regla práctica: gRPC para el backend interno (microservicios que se hablan entre sí, donde importa velocidad y contrato), REST para el edge (lo que consume el navegador o clientes externos). No compiten: se complementan en la misma arquitectura.
Para profundizar
- Documentación oficial de gRPC en Go: la referencia del equipo.
- Protocol Buffers (proto3): la sintaxis del contrato.
- golang-jwt: la librería de JWT para Go.
- go-playground/validator: validación de structs con tags.
- grpcurl: como curl, pero para gRPC.
- Ruta completa: Backend con Go.