PostgreSQL REST Services: Rust (Axum) vs. Node.js (Express)
Modern APIs serving single-page apps need three things: CORS done right, JWT verification, and a reliable connection to PostgreSQL. Below, two minimal patterns—Rust and Node—show how to implement a protected endpoint that returns a simple count from the database. After the snippets, you’ll find a pragmatic comparison on performance, ergonomics, and security.
Rust (Axum) — minimal pattern
Crates: axum
, tower-http
(CORS), jsonwebtoken
, sqlx
(Postgres).
Why sqlx
? Async, pool included, parameterized queries by default.
# Cargo.toml (essentials)
# 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();
// Query
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?;
// JWT public key (PEM)
let jwk = jsonwebtoken::DecodingKey::from_rsa_pem(include_bytes!("../rsa_public.pem"))?.into_static();
// CORS (lock to your 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) — minimal pattern
Packages: express
, cors
, jsonwebtoken
, pg
(or 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']
}));
// Connection pool
const pool = new Pool({
connectionString: 'postgres://app:secret@db:5432/mydb',
max: 10
});
// Protected endpoint
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'));
Performance and ergonomics
Latency and throughput
- Rust typically yields lower tail latency and more predictable throughput under load, thanks to zero-cost abstractions, a smaller runtime, and strong async IO.
- Node performs well for many workloads; with careful coding (no blocking work on the event loop), it scales effectively. CPU-heavy JWT verification and large JSON bodies can pressure the loop—offload when necessary.
Resource footprint
- Rust services tend to use less memory and CPU at equivalent throughput.
- Node is heavier per instance but can be scaled horizontally with process clustering.
Developer velocity
- Node offers rapid iteration and a vast middleware ecosystem.
- Rust is more exacting but gives stronger compile-time guarantees (types, lifetimes) and fewer runtime surprises.
Security notes (JWT + CORS + Postgres)
- CORS: Allow exactly your SPA origin; restrict methods and headers. Avoid
*
with credentials. - JWT: Pin algorithms (RS256/ES256), validate
iss
,aud
, andexp
. Use JWKS retrieval and cache bykid
; don’t hard-code a stale PEM in production. - Secrets: Load Postgres credentials and JWKS/PEM from a secrets manager (Vault, AWS Secrets Manager, Kubernetes Secrets).
- SQL safety: Use parameterized queries (both snippets do).
- Rate limiting and timeouts: Protect against token-brute forcing and connection pool exhaustion.
- TLS: Enforce HTTPS; use
sslmode=require
to encrypt Postgres traffic where applicable. - Observability: Log auth failures (rate-limited), pool saturation, and p95/p99 latency; never log raw JWTs.
When to choose which
Priority | Rust (Axum) | Node.js (Express) |
---|---|---|
Lowest tail latency | Strong choice | Good with careful loop hygiene |
Minimal footprint per instance | Strong choice | Heavier; scale with clustering |
Developer speed & ecosystem | Moderate, improving | Excellent (middleware, templates, tooling) |
Strict type safety at compile time | Excellent | Weaker; rely on runtime checks and TypeScript tooling |
Bottom line: If you want tight performance, small footprint, and strong safety guarantees, Rust is compelling. If your team optimizes for speed of delivery and a vast ecosystem, Node.js is a practical default—provided you are disciplined about event-loop safety, dependency hygiene, and strict JWT/CORS configuration.
Table of Contents
- Rust (Axum) — minimal pattern
- Node.js (Express) — minimal pattern
- Performance and ergonomics
- Security notes (JWT + CORS + Postgres)
- When to choose which
Trending
Table of Contents
- Rust (Axum) — minimal pattern
- Node.js (Express) — minimal pattern
- Performance and ergonomics
- Security notes (JWT + CORS + Postgres)
- When to choose which