A full-stack blog application written in Rust.
The project demonstrates a complete Rust backend with HTTP and gRPC APIs, PostgreSQL persistence, JWT authentication, a reusable client library, a CLI application, and an experimental WebAssembly frontend built with Leptos.
The backend provides user registration and authentication, as well as CRUD operations for blog posts. Public posts can be read without authentication, while creating, editing, and deleting posts requires a valid JWT token.
The WebAssembly frontend currently implements user registration and post creation.
- HTTP REST API with Actix Web
- gRPC API with Tonic
- PostgreSQL persistence with SQLx
- Database migrations
- JWT authentication
- Password hashing with Argon2
- Post CRUD operations
- Authorization checks for post modification
- Shared business logic between HTTP and gRPC APIs
- Structured logging with tracing
- Custom error handling with thiserror
- CORS support for the browser frontend
- HTTP client based on reqwest
- gRPC client based on Tonic
- JWT token management
- Authentication operations
- Post CRUD operations
- Pagination support
- User registration and login
- Create, get, update, delete and list posts
- HTTP and gRPC transports
- Local JWT token storage
- Command-line interface implemented with clap
The frontend is written in Rust using Leptos and compiled to WebAssembly.
Currently implemented:
- user registration
- JWT-based authenticated requests
- post creation
The backend URL used by the frontend is currently configured in index.html.
blog-project/
├── Cargo.toml
├── README.md
│
├── blog-server/
│ ├── Cargo.toml
│ ├── build.rs
│ ├── migrations/
│ ├── proto/
│ │ └── blog.proto
│ └── src/
│ ├── main.rs
│ ├── domain/
│ ├── application/
│ ├── data/
│ ├── infrastructure/
│ └── presentation/
│
├── blog-client/
│ ├── Cargo.toml
│ ├── build.rs
│ └── src/
│
├── blog-cli/
│ ├── Cargo.toml
│ └── src/
│
└── blog-wasm/
├── Cargo.toml
├── index.html
└── src/
The backend follows a layered architecture:
Presentation
│
▼
Application
│
▼
Domain
▲
│
Data / Infrastructure
Contains the core application models and domain errors:
UserPost- authentication request models
- post creation/update models
- domain-specific errors
Contains the application business logic:
- authentication service
- blog service
- authorization checks
The same application services are used by both the HTTP and gRPC presentation layers.
Contains PostgreSQL repository implementations for users and posts.
Contains infrastructure-specific functionality:
- PostgreSQL connection pool
- database migrations
- JWT generation and validation
- logging configuration
Contains:
- Actix Web HTTP handlers
- JWT middleware
- Tonic gRPC service implementation
| Component | Technology |
|---|---|
| Language | Rust |
| HTTP server | Actix Web |
| gRPC | Tonic / Prost |
| Database | PostgreSQL |
| Database access | SQLx |
| Authentication | JWT |
| Password hashing | Argon2 |
| Serialization | Serde |
| Error handling | thiserror / anyhow |
| Logging | tracing |
| HTTP client | reqwest |
| CLI | clap |
| WASM | wasm-pack |
| Frontend | Leptos |
| Protocol | Protocol Buffers |
Public HTTP endpoints:
POST /api/auth/register
POST /api/auth/login
Successful authentication returns a JWT token.
Public endpoints:
GET /api/posts
GET /api/posts/{id}
Authenticated endpoints:
POST /api/posts
PUT /api/posts/{id}
DELETE /api/posts/{id}
Authenticated requests use the standard Bearer token header:
Authorization: Bearer <JWT_TOKEN>Only the author of a post can update or delete it.
The gRPC service provides the same core functionality:
Register
Login
CreatePost
GetPost
UpdatePost
DeletePost
ListPosts
Public methods:
Register
Login
GetPost
ListPosts
Authenticated methods:
CreatePost
UpdatePost
DeletePost
JWT tokens for protected gRPC operations are passed through request metadata:
authorization: Bearer <JWT_TOKEN>
The Protocol Buffers schema is defined in:
blog-server/proto/blog.proto
and is used for generating the server and client implementations.
The application stores its data in PostgreSQL.
The main tables are:
id
username
email
password_hash
created_at
id
title
content
author_id
created_at
updated_at
posts.author_id references users.id.
Database schema changes are managed through SQLx migrations.
The project requires:
- Rust and Cargo
- PostgreSQL
sqlx-cli- Protocol Buffers compiler (
protoc) wasm-pack- Trunk
Install SQLx CLI if necessary:
cargo install sqlx-cli --no-default-features --features postgresInstall wasm-pack:
cargo install wasm-packInstall Trunk:
cargo install --locked trunkThe backend uses environment variables for its configuration.
For local testing, the project uses .env.test.
The database connection used in the examples below is:
postgres://test_user:test_password@localhost:5433/test_db
The environment must also provide the JWT secret expected by the backend.
Do not commit files containing real secrets to Git.
Run database commands from the directory where the migrations directory is available to SQLx.
DATABASE_URL="postgres://test_user:test_password@localhost:5433/test_db" sqlx database resetThis recreates the database and applies the configured database setup from scratch.
To apply migrations without resetting the database:
DATABASE_URL="postgres://test_user:test_password@localhost:5433/test_db" sqlx migrate runBuild the backend with the database URL available to SQLx:
DATABASE_URL="postgres://test_user:test_password@localhost:5433/test_db" cargo buildLoad the test environment and start the compiled server:
export $(grep -v '^#' .env.test | xargs)
./target/debug/blog-serverThe backend starts both the HTTP and gRPC services.
The application exposes:
HTTP API: localhost:8080
gRPC API: localhost:50051
Build the frontend from the WASM crate:
wasm-pack build --target webThe backend URL used by the frontend is configured in index.html.
Make sure it points to the running HTTP backend before opening the frontend.
Trunk is used for serving the frontend locally and can be installed with:
cargo install --locked trunkcurl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"email": "alice@example.com",
"password": "secret123"
}'The response contains the authenticated user and a JWT token.
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"password": "secret123"
}'Save the returned token for authenticated requests:
export TOKEN="<JWT_TOKEN>"curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"title": "My first post",
"content": "Hello from Rust!"
}'curl "http://localhost:8080/api/posts?limit=10&offset=0"curl http://localhost:8080/api/posts/1curl -X PUT http://localhost:8080/api/posts/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"title": "Updated title",
"content": "Updated content"
}'curl -X DELETE http://localhost:8080/api/posts/1 \
-H "Authorization: Bearer $TOKEN"The CLI uses the blog-client crate and can communicate with the backend through HTTP or gRPC.
cargo run -p blog-cli -- register \
--username alice \
--email alice@example.com \
--password secret123cargo run -p blog-cli -- login \
--username alice \
--password secret123cargo run -p blog-cli -- create \
--title "My first post" \
--content "Created from the CLI"cargo run -p blog-cli -- get --id 1cargo run -p blog-cli -- list --limit 20 --offset 0cargo run -p blog-cli -- update \
--id 1 \
--title "Updated title" \
--content "Updated content"cargo run -p blog-cli -- delete --id 1The CLI can use gRPC instead of HTTP with the --grpc option.
Example:
cargo run -p blog-cli -- --grpc create \
--title "gRPC post" \
--content "Created using Tonic"The frontend is implemented with Leptos.
Unlike the native blog-client, the browser frontend communicates directly with the HTTP API.
The currently implemented frontend flow is:
Browser
│
├── Register user
│ │
│ ▼
│ HTTP API
│ │
│ ▼
│ JWT token
│
└── Create post
│
│ Authorization: Bearer <token>
▼
HTTP API
At the current stage, the Leptos frontend implements registration and authenticated post creation. The remaining backend functionality is available through the HTTP API, gRPC API, client library, and CLI.
Passwords are hashed with Argon2 before they are stored in PostgreSQL.
After successful registration or login, the server generates a JWT token identifying the user.
Protected HTTP operations use:
Authorization: Bearer <token>
Protected gRPC operations use the equivalent token in gRPC metadata.
The backend verifies both authentication and post ownership before allowing posts to be modified or deleted.
Application-specific errors are defined with thiserror and converted to transport-specific responses.
Examples include:
Invalid credentials -> 401 / UNAUTHENTICATED
Unauthorized -> 401 / UNAUTHENTICATED
Forbidden -> 403 / PERMISSION_DENIED
Post not found -> 404 / NOT_FOUND
User already exists -> 409 / ALREADY_EXISTS
Invalid request -> 400 / INVALID_ARGUMENT
The server uses tracing for structured application logging.
The log level can be configured through RUST_LOG, for example:
RUST_LOG=debug ./target/debug/blog-server