// 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.
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
// 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
| Spring | Go | Nota |
|---|---|---|
@RestController + @GetMapping | mux.HandleFunc("GET /ruta", fn) | Rutas explícitas en un solo lugar, sin escaneo de anotaciones |
@PathVariable | r.PathValue("id") | Stdlib desde 1.22 |
@RequestParam | r.URL.Query().Get("q") | Explícito; valida tú |
@RequestBody + Jackson | json.NewDecoder(r.Body).Decode(&dto) | Con DisallowUnknownFields() para APIs estrictas |
Filter / HandlerInterceptor | Middleware (función que envuelve) | Composición de funciones, sin registro |
@ControllerAdvice | Middleware de recover + helper de errores | Lo escribes una vez, ~15 líneas |
| Inyección de dependencias | Structs + argumentos en main() | El "contenedor" es tu función main; todo rastreable con ctrl+click |
| Tomcat embebido | http.Server | Ya está dentro; sin servlet container |
| application.yml | Variables de entorno + flags (o viper) | 12-factor por defecto |
spring-boot-starter-validation | go-playground/validator | Tags validate:"required,email" en el struct |
¿Y los frameworks?
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.
Gin
Rápido, con contexto propio, binding y validación integrados. Mucho tutorial y mucha oferta laboral lo mencionan. API estilo Express.
Echo
Similar a Gin: router veloz, middleware rico, buena documentación. Elección sólida para APIs medianas.
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)
// 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
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
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
// 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
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
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.
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.
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".