// 06 · tu territorio

HTTP, APIs y seguridad

Aquí es donde Spring y Go se miran a los ojos. La stdlib trae un servidor de producción; tú traes los años de APIs REST.

// sin frameworks, en serio

El servidor HTTP de la stdlib

No es "un servidor de juguete para demos": es el servidor que usan miles de servicios reales. Desde Go 1.22, el router estándar () entiende métodos y variables de ruta, así que para muchas APIs no necesitas ni framework.

api.go — una API sin frameworks
func main() {
	mux := http.NewServeMux()

	// método + ruta + variables, directo en la stdlib (Go 1.22+)
	mux.HandleFunc("GET /productos", listarProductos)
	mux.HandleFunc("GET /productos/{id}", verProducto)
	mux.HandleFunc("POST /productos", crearProducto)

	log.Println("escuchando en :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

// Un handler: recibe writer y request. Esto es un @GetMapping sin anotaciones.
func verProducto(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id") // la variable {id} de la ruta

	p, err := buscarProducto(id)
	if err != nil {
		http.Error(w, "no encontrado", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(p) // struct → JSON (tu Jackson integrado)
}

// Los tags del struct controlan el JSON, como @JsonProperty:
type Producto struct {
	ID      int     `json:"id"`
	Nombre  string  `json:"nombre"`
	Precio  float64 `json:"precio"`
	interno string  `json:"-"` // el guion lo excluye del JSON
}

Middleware: tus Filters, como funciones puras

middleware.go
// Un middleware es una función que envuelve un http.Handler y devuelve otro.
// Sin interfaces especiales, sin registro en contenedores: composición pura.
func conLog(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		inicio := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s (%v)", r.Method, r.URL.Path, time.Since(inicio))
	})
}

func conAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") == "" {
			http.Error(w, "no autorizado", http.StatusUnauthorized)
			return // corta la cadena, como filterChain sin doFilter
		}
		next.ServeHTTP(w, r)
	})
}

// Se encadenan como capas de cebolla:
handler := conLog(conAuth(mux))

Spring ↔ Go: el diccionario

SpringGoNota
@RestController + @GetMappingmux.HandleFunc("GET /ruta", fn)Rutas explícitas en un solo lugar, sin escaneo de anotaciones
@PathVariabler.PathValue("id")Stdlib desde 1.22
@RequestParamr.URL.Query().Get("q")Explícito; valida tú
@RequestBody + Jacksonjson.NewDecoder(r.Body).Decode(&dto)Con DisallowUnknownFields() para APIs estrictas
Filter / HandlerInterceptorMiddleware (función que envuelve)Composición de funciones, sin registro
@ControllerAdviceMiddleware de recover + helper de erroresLo escribes una vez, ~15 líneas
Inyección de dependenciasStructs + argumentos en main()El "contenedor" es tu función main; todo rastreable con ctrl+click
Tomcat embebidohttp.ServerYa está dentro; sin servlet container
application.ymlVariables de entorno + flags (o viper)12-factor por defecto
spring-boot-starter-validationgo-playground/validatorTags validate:"required,email" en el struct

¿Y los frameworks?

minimalista · el favorito idiomático

chi

Router 100% compatible con net/http. Añade subrouters y middleware sobre la stdlib sin inventar nada. Si vienes de Spring, empieza aquí o con stdlib pura.

el más popular

Gin

Rápido, con contexto propio, binding y validación integrados. Mucho tutorial y mucha oferta laboral lo mencionan. API estilo Express.

completo

Echo

Similar a Gin: router veloz, middleware rico, buena documentación. Elección sólida para APIs medianas.

estilo Express literal

Fiber

API calcada de Express.js sobre fasthttp. Familiar si vienes de Node, pero se aparta de net/http — menos idiomático.

Cliente HTTP bien hecho (timeouts o muerte)
cliente.go
// NUNCA uses http.Get "pelado" en producción: no tiene timeout.
var cliente = &http.Client{Timeout: 8 * time.Second}

func obtenerUsuario(ctx context.Context, id int) (Usuario, error) {
	var u Usuario

	url := fmt.Sprintf("https://api.ejemplo.com/usuarios/%d", id)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return u, err
	}

	res, err := cliente.Do(req)
	if err != nil {
		return u, fmt.Errorf("llamando a usuarios: %w", err)
	}
	defer res.Body.Close() // SIEMPRE, o fugas de conexiones

	if res.StatusCode != http.StatusOK {
		return u, fmt.Errorf("usuarios respondió %d", res.StatusCode)
	}
	return u, json.NewDecoder(res.Body).Decode(&u)
}

// nuevo · el kit de seguridad práctico

Seguridad: contraseñas, JWT, CORS y rate limiting

Lo que Spring Security te da (y te esconde) en un starter, en Go se escribe a la vista en pocas líneas. Ganas en comprensión lo que pierdes en magia.

Contraseñas con bcrypt

password.go
import "golang.org/x/crypto/bcrypt"

// Al registrar: guarda el hash, jamás la contraseña
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

// Al hacer login: compara en tiempo constante (inmune a timing attacks)
err = bcrypt.CompareHashAndPassword(hashGuardado, []byte(passwordRecibida))
if err != nil {
	// contraseña incorrecta: mismo mensaje genérico que "usuario no existe"
	http.Error(w, "credenciales inválidas", http.StatusUnauthorized)
	return
}

JWT: emitir y verificar

jwt.go — golang-jwt/jwt/v5
import "github.com/golang-jwt/jwt/v5"

var secreto = []byte(os.Getenv("JWT_SECRET"))

// Al hacer login, emite el token (tu TokenProvider de Spring Security):
func emitirToken(userID int) (string, error) {
	claims := jwt.MapClaims{
		"sub": userID,
		"exp": time.Now().Add(24 * time.Hour).Unix(),
		"iat": time.Now().Unix(),
	}
	return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secreto)
}

// Middleware que protege rutas (tu JwtAuthenticationFilter):
func conJWT(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		crudo := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")

		token, err := jwt.Parse(crudo, func(t *jwt.Token) (any, error) {
			return secreto, nil
		}, jwt.WithValidMethods([]string{"HS256"})) // rechaza alg:none y amigos

		if err != nil || !token.Valid {
			http.Error(w, "token inválido", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}
!

Los tres errores clásicos con 1) No fijar WithValidMethods (ataque alg:none). 2) Secreto corto o hardcodeado — mínimo 32 bytes aleatorios desde variable de entorno. 3) Tokens eternos: pon exp corto y usa refresh tokens si necesitas sesiones largas.

CORS para tu frontend React

cors.go
// El middleware CORS que en Spring era @CrossOrigin, a mano y sin misterio:
func conCORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE")
		w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")

		if r.Method == http.MethodOptions { // preflight del navegador
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next.ServeHTTP(w, r)
	})
}
// En producción: valida el Origin contra una lista blanca,
// o usa la lib rs/cors si prefieres no mantener esto.

Rate limiting

ratelimit.go — golang.org/x/time/rate
import "golang.org/x/time/rate"

// Token bucket: 10 requests/segundo con ráfagas de hasta 20.
var limitador = rate.NewLimiter(10, 20)

func conLimite(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !limitador.Allow() {
			http.Error(w, "demasiadas peticiones", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}
// Para límite POR IP: un map[string]*rate.Limiter protegido con mutex,
// limpiado periódicamente — 30 líneas; o usa la lib ulule/limiter.
Validación de entrada con go-playground/validator
validacion.go
import "github.com/go-playground/validator/v10"

type RegistroDTO struct {
	Nombre   string `json:"nombre"   validate:"required,min=2,max=60"`
	Email    string `json:"email"    validate:"required,email"`
	Password string `json:"password" validate:"required,min=10"`
	Edad     int    `json:"edad"     validate:"gte=18"`
}

var valida = validator.New()

func crearCuenta(w http.ResponseWriter, r *http.Request) {
	var in RegistroDTO
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		http.Error(w, "JSON inválido", http.StatusBadRequest)
		return
	}
	if err := valida.Struct(in); err != nil {
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
		return
	}
	// ... crear el usuario
}

Los tags te resultarán idénticos a las anotaciones de Bean Validation (@NotNull, @Email, @Min) — es la misma idea, en la misma posición.

// nuevo · para tu lado React

Servir tu SPA de React desde Go

El combo clásico para tu perfil: React (Vite) para el frontend, Go para la API — y el build de React embebido dentro del binario Go. Un solo ejecutable contiene tu app completa: frontend, API y assets.

spa.go — embed + fallback para React Router
import (
	"embed"
	"io/fs"
	"net/http"
	"strings"
)

// El resultado de `npm run build` copiado a frontend/dist:
//
//go:embed frontend/dist
var ui embed.FS

func spaHandler() http.Handler {
	dist, _ := fs.Sub(ui, "frontend/dist")
	archivos := http.FileServerFS(dist)

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ruta := strings.TrimPrefix(r.URL.Path, "/")

		// ¿El archivo existe (JS, CSS, imágenes)? Sírvelo tal cual.
		if _, err := fs.Stat(dist, ruta); err == nil && ruta != "" {
			archivos.ServeHTTP(w, r)
			return
		}
		// Si no existe, es una ruta de React Router (/perfil, /tareas/3):
		// devuelve index.html y deja que React tome el control.
		http.ServeFileFS(w, r, dist, "index.html")
	})
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("GET /api/", http.StripPrefix("/api", rutasAPI())) // tu API
	mux.Handle("GET /", spaHandler())                              // tu React

	http.ListenAndServe(":8080", mux)
}
✓

En desarrolloNo embebas nada: corre Vite en :5173 con server.proxy apuntando /api a tu Go en :8080 (igual que hacías con Spring). El embed es solo para el build de producción — y elimina el Nginx que antes necesitabas para servir estáticos.

// tiempo real

WebSockets y tiempo real

Para tiempo real, Go es de lo mejor que hay: cada conexión WebSocket es simplemente una goroutine leyendo y otra escribiendo — 10.000 clientes conectados son 20.000 goroutines baratas, no 10.000 threads.

Las opciones: gorilla/websocket (el clásico, más usado) o coder/websocket (más moderno y minimalista). Para eventos solo servidor→cliente, considera Server-Sent Events: no necesita ni librería, es HTTP puro.

i

Proyecto completo en la página de appsEn Apps de ejemplo está el proyecto 07: un chat en tiempo real con el patrón hub (canales para registrar clientes y difundir mensajes, cero mutex). Es el ejemplo canónico de "compartir comunicando".