category

Web ApplicationDatabaseMachine learningKuberneteseCommerceCloud

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, and exp. Use JWKS retrieval and cache by kid; 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

PriorityRust (Axum)Node.js (Express)
Lowest tail latencyStrong choiceGood with careful loop hygiene
Minimal footprint per instanceStrong choiceHeavier; scale with clustering
Developer speed & ecosystemModerate, improvingExcellent (middleware, templates, tooling)
Strict type safety at compile timeExcellentWeaker; 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

Serverless Database Showdown: Oracle, Azure, Redshift, and AuroraOrchestrating Spark on AWS EMR from Apache Airflow — The Low-Ops WayCase Study: A Lightweight Intrusion Detection System with OpenFaaS and PyTorchBuilding Resilient Kubernetes Clusters with Portworx Community EditionIntegrating Shopify into a Next.js React Web App