🔎 Buscar

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

Wiki / Apuntes📖 Contenido

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.ServeMux con 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 con r.PathValue, y un método incorrecto devuelve 405 automá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 none o con un algoritmo equivocado). La comprobación if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok dentro del keyfunc de jwt.Parse evita 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.