Origin. Assignment from the Yandex Practicum Rust course (capstone-style): one workspace, four crates sharing a domain layer. Developed locally, published as a snapshot.
A small blog platform in Rust, laid out as a Cargo workspace of four crates:
| Crate | Type | Purpose |
|---|---|---|
blog-server |
binary | Backend serving the same logic over HTTP (actix-web, :8080) and gRPC (tonic, :50051); storage - PostgreSQL. |
blog-client |
library | A reusable client with a single BlogClient API over HTTP or gRPC. |
blog-cli |
binary | A console client built on blog-client. |
blog-wasm |
cdylib | A browser frontend (wasm-bindgen) that talks to the HTTP API. |
The server follows clean architecture principles:
blog-server/src/
├── domain/ # Pure types and errors (User, Post, AppError)
├── application/ # Business logic (AuthService, BlogService) - shared by HTTP and gRPC
├── data/ # PostgreSQL repositories (parameterized sqlx queries)
├── infrastructure/ # Config, DB pool, JWT, tracing
└── presentation/ # HTTP handlers + middleware (actix), gRPC service (tonic)
- The HTTP and gRPC layers call the same
AuthService/BlogService(wrapped inArc) - no logic duplication. - Passwords are hashed with Argon2; access is protected by JWT (24 hours, with
user_id+usernamein the claims). - The gRPC schema lives in a single file,
blog-server/proto/blog.proto; the client'sbuild.rsreferences it by relative path (no copy). protocis not required on the machine - the build scripts use a vendored binary (protoc-bin-vendored).- The server uses sqlx runtime queries, so
cargo buildworks without a database connection.
- Rust 1.75+ (
rustup,cargo). - PostgreSQL - easiest via Docker (see below).
protocis not needed. - For the WASM frontend only:
rustup target add wasm32-unknown-unknowncargo install wasm-pack- any static file server (for example, Python's
http.server).
Start PostgreSQL (Docker):
docker run -d --name blog-pg \
-e POSTGRES_USER=blog -e POSTGRES_PASSWORD=blog -e POSTGRES_DB=blog \
-p 5432:5432 postgres:16Migrations are applied automatically on server startup (see blog-server/migrations/).
Copy the example env file and adjust the values if needed:
cp blog-server/.env.example blog-server/.envblog-server/.env:
DATABASE_URL=postgres://blog:blog@localhost:5432/blog
JWT_SECRET=please-change-me-to-a-long-random-secret-min-32-charsYou can generate a strong secret with, for example, openssl rand -hex 32. Do not commit .env (it's in .gitignore).
cargo build --workspacecargo run --bin blog-server
# HTTP API -> http://localhost:8080
# gRPC API -> http://localhost:50051The CLI uses HTTP by default; add --grpc for gRPC and --server <url> to override the address.
After register/login, the JWT is saved to a .blog_token file and reused automatically.
cargo run --bin blog-cli -- register --username ivan --email ivan@example.com --password secret123
cargo run --bin blog-cli -- login --username ivan --password secret123
cargo run --bin blog-cli -- create --title "My first post" --content "Hello, world"
cargo run --bin blog-cli -- get --id 1
cargo run --bin blog-cli -- list --limit 20 --offset 0
cargo run --bin blog-cli -- update --id 1 --title "Updated title"
cargo run --bin blog-cli -- delete --id 1
# the same commands over gRPC:
cargo run --bin blog-cli -- --grpc create --title "Via gRPC" --content "..."# Register (201) -> { token, user }
curl -s -X POST http://localhost:8080/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"username":"ivan","email":"ivan@example.com","password":"secret123"}'
# Login (200)
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"ivan","password":"secret123"}' | jq -r .token)
# Create a post (201, authentication required)
curl -s -X POST http://localhost:8080/api/posts \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Hello","content":"World"}'
# Public read
curl -s http://localhost:8080/api/posts/1
curl -s 'http://localhost:8080/api/posts?limit=10&offset=0'
# Update (200) / delete (204) - require a token and authorship
curl -s -X PUT http://localhost:8080/api/posts/1 \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Hello (edited)","content":"World"}'
curl -s -X DELETE http://localhost:8080/api/posts/1 -H "Authorization: Bearer $TOKEN"| Method | Path | Auth | Result |
|---|---|---|---|
| POST | /api/auth/register |
– | 201 {token,user} / 409 |
| POST | /api/auth/login |
– | 200 {token,user} / 401 |
| POST | /api/posts |
Bearer | 201 post |
| GET | /api/posts/{id} |
– | 200 post / 404 |
| PUT | /api/posts/{id} |
Bearer | 200 / 404 / 403 |
| DELETE | /api/posts/{id} |
Bearer | 204 / 404 / 403 |
| GET | /api/posts?limit=&offset= |
– | 200 {posts,total,limit,offset} |
The gRPC service mirrors these operations (Register, Login, CreatePost, GetPost, UpdatePost,
DeletePost, ListPosts); write RPCs read the JWT from the authorization: Bearer <token> metadata.
rustup target add wasm32-unknown-unknown # once
cargo install wasm-pack # once
cd blog-wasm
wasm-pack build --target web # output in blog-wasm/pkg/
cd ..
python -m http.server 8000 # serve the workspace root
# open http://localhost:8000On the page you can register/log in (the JWT is stored in localStorage), see the public feed,
and create/edit/delete your own posts. Make sure the server is running on :8080.
blog-project/
├── Cargo.toml # workspace + shared dependency versions
├── index.html # host page for WASM
├── blog-server/ # HTTP + gRPC backend (clean architecture)
├── blog-client/ # HTTP/gRPC client library
├── blog-cli/ # CLI built on blog-client
└── blog-wasm/ # wasm-bindgen frontend
Cargo.lock,.env,.blog_token,target/, andblog-wasm/pkg/are in.gitignore.- Only unary gRPC is used (no streaming).
- Logs go through
tracing; for verbose output setRUST_LOG=debug.