Servicios REST con PostgreSQL: Rust (Axum) vs. Node.js (Express)
Las APIs modernas que sirven aplicaciones de una sola página necesitan tres cosas: CORS bien configurado, verificación JWT y una conexión confiable a PostgreSQL. A continuación, se presentan dos patrones mínimos —Rust y Node— que muestran cómo implementar un endpoint protegido que devuelve un conteo simple desde la base de datos. Después de los fragmentos de código, encontrarás una comparación pragmática sobre rendimiento, ergonomía y seguridad.
Rust (Axum) — patrón mínimo
Crates: axum
, tower-http
(CORS), jsonwebtoken
, sqlx
(Postgres).
¿Por qué sqlx
? Asíncrono, con pool incluido y consultas parametrizadas por defecto.
# Cargo.toml (esenciales)
# axum = "0.7"
# tower-http = { version = "0.5", features = ["cors"] }
# jsonwebtoken = "9"
# sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
# serde = { version = "1", features = ["derive"] }
# tokio = { version = "1", features = ["full"] }
use axum::{routing::get, extract::State, http::{HeaderMap, Method}, Json, Router};
use tower_http::cors::CorsLayer;
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use sqlx::{Pool, Postgres};
use serde::Deserialize;
use std::{sync::Arc, net::SocketAddr};
#[derive(Clone)]
struct AppState {
pool: Arc<Pool<Postgres>>,
jwk: DecodingKey<'static>,
}
#[derive(Deserialize)]
struct Claims { sub: String, iss: String, aud: String, exp: usize }
async fn employees_count(State(s): State<AppState>, headers: HeaderMap) -> Json<serde_json::Value> {
// JWT: Authorization: Bearer <token>
let token = headers.get("authorization").and_then(|v| v.to_str().ok()).unwrap_or("");
let token = token.strip_prefix("Bearer ").unwrap_or("");
let _claims = decode::<Claims>(token, &s.jwk, &Validation::new(Algorithm::RS256)).unwrap();
// Consulta
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM employees")
.fetch_one(&*s.pool)
.await
.unwrap();
Json(serde_json::json!({ "employees": count }))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Pool
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(10)
.connect("postgres://app:secret@db:5432/mydb")
.await?;
// Clave pública JWT (PEM)
let jwk = jsonwebtoken::DecodingKey::from_rsa_pem(include_bytes!("../rsa_public.pem"))?.into_static();
// CORS (restringido a tu SPA)
let cors = CorsLayer::new()
.allow_origin("https://app.example.com".parse()?)
.allow_methods([Method::GET])
.allow_headers(["authorization".parse()?]);
let app = Router::new()
.route("/api/employees/count", get(employees_count))
.with_state(AppState { pool: Arc::new(pool), jwk })
.layer(cors);
let addr: SocketAddr = "0.0.0.0:8080".parse()?;
axum::Server::bind(&addr).serve(app.into_make_service()).await?;
Ok(())
}
Node.js (Express) — patrón mínimo
Paquetes: express
, cors
, jsonwebtoken
, pg
(o pg-pool
).
// npm i express cors jsonwebtoken pg
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const { Pool } = require('pg');
const app = express();
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET'],
allowedHeaders: ['Authorization']
}));
// Pool de conexiones
const pool = new Pool({
connectionString: 'postgres://app:secret@db:5432/mydb',
max: 10
});
// Endpoint protegido
app.get('/api/employees/count', async (req, res) => {
try {
const token = (req.headers.authorization || '').replace(/^Bearer\s+/,'');
const claims = jwt.verify(token, process.env.RSA_PUBLIC_PEM, {
algorithms: ['RS256'],
audience: 'your-aud',
issuer: 'your-iss'
});
const { rows } = await pool.query('SELECT COUNT(*)::bigint AS c FROM employees');
res.json({ employees: Number(rows[0].c), user: claims.sub });
} catch (err) {
res.status(401).json({ error: 'unauthorized' });
}
});
app.listen(8080, () => console.log('listening on 8080'));
Rendimiento y ergonomía
Latencia y rendimiento
- Rust suele ofrecer menor latencia de cola y un rendimiento más predecible bajo carga, gracias a sus abstracciones de costo cero, su runtime ligero y su IO asíncrono.
- Node funciona bien en muchos escenarios; con código disciplinado (sin bloqueo en el event loop), escala de forma efectiva. La verificación JWT intensiva en CPU y grandes cuerpos JSON pueden presionar el bucle: conviene descargarlos en procesos/hilos separados.
Consumo de recursos
- Los servicios en Rust tienden a usar menos memoria y CPU a igual rendimiento.
- Node es más pesado por instancia, pero se escala fácilmente de manera horizontal con clustering.
Velocidad de desarrollo
- Node ofrece iteración rápida y un ecosistema de middlewares muy amplio.
- Rust es más exigente, pero brinda garantías sólidas en tiempo de compilación (tipos, lifetimes) y menos sorpresas en ejecución.
Notas de seguridad (JWT + CORS + Postgres)
- CORS: permite únicamente el origen de tu SPA; restringe métodos y cabeceras. Evita
*
con credenciales. - JWT: fija algoritmos (RS256/ES256), valida
iss
,aud
yexp
. Usa JWKS y cachea porkid
; no dejes un PEM codificado en duro en producción. - Secrets: carga credenciales de Postgres y claves JWT desde un gestor de secretos (Vault, AWS Secrets Manager, Secrets de Kubernetes).
- Seguridad SQL: utiliza siempre consultas parametrizadas (como en los ejemplos).
- Limitación y timeouts: protege contra fuerza bruta de tokens y agotamiento del pool.
- TLS: exige HTTPS; usa
sslmode=require
para cifrar tráfico Postgres cuando sea posible. - Observabilidad: registra fallos de auth (con límite), saturación del pool y latencias p95/p99; nunca loguees JWT sin enmascarar.
Cuándo elegir cada uno
Prioridad | Rust (Axum) | Node.js (Express) |
---|---|---|
Latencia más baja | Muy buen candidato | Correcto con disciplina en el event loop |
Huella mínima por instancia | Muy buen candidato | Más pesado; escalar con clustering |
Velocidad y ecosistema dev | Media, en crecimiento | Excelente (middlewares, plantillas, tooling) |
Seguridad de tipos en compilación | Excelente | Más débil; depende de controles en runtime/TS |
En resumen: Si buscas máximo rendimiento, baja huella y fuertes garantías de seguridad, Rust es convincente. Si tu equipo prioriza la velocidad de entrega y un ecosistema amplio, Node.js sigue siendo un buen punto de partida — siempre que seas riguroso con el event loop, la higiene de dependencias y la configuración estricta de JWT/CORS.
Table of Contents
- Rust (Axum) — patrón mínimo
- Node.js (Express) — patrón mínimo
- Rendimiento y ergonomía
- Notas de seguridad (JWT + CORS + Postgres)
- Cuándo elegir cada uno
Trending
Table of Contents
- Rust (Axum) — patrón mínimo
- Node.js (Express) — patrón mínimo
- Rendimiento y ergonomía
- Notas de seguridad (JWT + CORS + Postgres)
- Cuándo elegir cada uno