1
Fork 0
rust-crud-sql-api/README.md
Leonardo Devai 60afb61651 Review and modernize all 42 projects to the updated standard
Pinned 2026 toolchains (Go 1.26, Rust 1.98/edition 2024, Python 3.14 + uv, Node 24,
Zig 0.16, NixOS 26.05), postgres 18 / mongo 8, lockfiles built from, non-root
runtimes, .dockerignore, per-project LICENSE, READMEs with the git.devai.io clone
line, checkout@v7 CI. Security fixes in the legacy Rust APIs (any-password login,
self-assigned admin, hard-coded JWT secret), JWT alg/exp/sub enforcement across the
blog series, safe markdown links in the frontends, and many smaller bugs — every
project was built, run and exercised end to end.

Adds scripts/publish.sh + a CI publish job that splits every folder into its own
repo at git.devai.io/templates/<folder>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G
2026-09-27 21:10:38 +02:00

6.3 KiB

rust-crud-sql-api

A JWT-secured REST API in Rust: warp filters, Postgres through sqlx, argon2 passwords and role-based routes (User / Admin) — users, articles and comments, laid out as routes → handlers → service per module.

Run

git clone https://git.devai.io/templates/rust-crud-sql-api.git
cd rust-crud-sql-api
docker compose up --build

The API answers on http://localhost:8080 (curl localhost:8080/health → ok). Postgres keeps its state in ./data/postgres; schema.sql is applied on every start (IF NOT EXISTS), so there is no migrate step.

Without Docker: run a Postgres, export the variables from .env.example, then cargo run (Rust 1.98, the toolchain the Dockerfile pins).

How it works

Method Path Auth Result
GET /health — 200 ok
POST /api/auth/register — {email, name, password} → 201 user (role User), 409 if taken
POST /api/auth/login — {email, password} → 200 {id, email, name, role, access_token}
GET /api/articles — 200 all articles, without content
GET /api/articles_home — 200 articles with in_home: true, without content
GET /api/articles/{url} — 200 full article, or 404
POST /api/articles Admin {title, url, content?, tags?, in_home?} → 201, 409 if the url exists
PUT /api/articles Admin same body plus id → 200, or 404
DELETE /api/articles/{id} Admin 204 (its comments go with it), or 404
PUT /api/articles/updateHomeView/{id} Admin flips in_home → 200 article
GET /api/articles/comments/{article_id} — 200 comments, oldest first
POST /api/articles/comments — {article_id, author, email, content} → 201, 404 for an unknown article
DELETE /api/articles/comments/{article_id}/{id} Admin 204, or 404
GET /api/users Admin 200 all users
GET /api/users/{id} Admin 200 user, or 404
POST /api/users Admin {email, name, password, role?} → 201
PUT /api/users Admin {id, email, name, role} → 200
DELETE /api/users/{id} Admin 204, or 404
PUT /api/users/changePassword any {id, current_password?, new_password} → 204
  • Auth — login returns an HS256 JWT signed with AUTH_SECRET (sub = user id, role, 24 h expiry); send it as Authorization: Bearer <token>. Verification pins HS256 and requires exp. with_auth(env, Role::Admin) in src/auth/middleware.rs is the warp filter that answers 401 without a valid token and 403 when the role is not enough.

  • Passwords — argon2id (RustCrypto argon2), hashed on tokio's blocking pool. Users change their own password with current_password; admins can reset anyone's.

  • Roles — registration always creates a User. Promote the first admin in the database, then log in again (the role travels inside the JWT):

    docker compose exec db psql -U demo -d demo \
      -c "UPDATE users SET role = 'Admin' WHERE email = 'admin@test.com'"
    
  • Comments are public to read and write; the commenter's email is stored but never returned.

  • Errors are always JSON: {"error": "message"}, including warp's own rejections (bad JSON 400, wrong content-type 415, wrong method 405).

Try it:

curl -X POST localhost:8080/api/auth/register -H 'content-type: application/json' \
  -d '{"email":"admin@test.com","name":"Admin","password":"supersecret"}'
# promote it with the psql command above, then:
TOKEN=$(curl -s localhost:8080/api/auth/login -H 'content-type: application/json' \
  -d '{"email":"admin@test.com","password":"supersecret"}' | jq -r .access_token)
curl -X POST localhost:8080/api/articles -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' -d '{"title":"Hello","url":"hello","content":"First post"}'
curl localhost:8080/api/articles/hello

Layout

src/main.rs          route tree, request log, graceful shutdown
src/environment.rs   config, Postgres pool, schema bootstrap, shared filters
src/error.rs         ApiError and the rejection → {"error": ...} mapping
src/auth/            JWT + argon2 (mod.rs), with_auth filter, register/login
src/users/           routes → handlers → service (sqlx queries)
src/articles/        routes → handlers → service, comments included
schema.sql           tables and indexes, applied on startup

Deploy

Push to your own GitHub repo and the shipped workflow (.github/workflows/ci.yml) tests the compose stack, publishes the image to GHCR, and — once you set the DEPLOY_HOST / DEPLOY_USER variables and DEPLOY_KEY secret — deploys it to your server over ssh. Set a long random AUTH_SECRET on the server; the one in compose.yaml is for local use only.


Part of devai.io — Rust API boilerplates. Same API on MongoDB: rust-crud-nosql-api; actix-web take: rust-crud-actix-mongo-api.