This file provides context for Claude Code when working on this codebase.
This is a fullstack monorepo with:
- Backend: Rust (Axum) with REST + WebSocket APIs
- Frontend: Next.js (React 19) with TypeScript
- Database: PostgreSQL with SQLx
- Type Generation: Automatic TypeScript SDK from Rust OpenAPI schemas
apps/backend/src/
├── api/
│ ├── rest/ # REST API handlers
│ │ ├── mod.rs # Router setup + OpenAPI config
│ │ ├── health.rs # Health check endpoint
│ │ └── todos.rs # RPC-style unified endpoint
│ └── ws/ # WebSocket handlers
│ ├── mod.rs # WebSocket exports
│ └── handler.rs # WS connection handling + broadcast
├── bin/
│ └── generate_openapi.rs # Binary to generate OpenAPI spec
├── db/
│ ├── mod.rs # Database abstraction
│ └── pg/ # PostgreSQL implementation
│ ├── mod.rs
│ ├── todos.rs # Todo database operations
│ └── migrations/ # SQL migration files
├── models/
│ ├── mod.rs # Model exports
│ ├── domain.rs # Domain models (data structures with sqlx::FromRow)
│ └── api.rs # API types (REST + WebSocket messages)
├── errors.rs # Error handling
├── lib.rs # Library entry point
└── main.rs # Server entry point
- Single Endpoint:
POST /api/todos - Pattern: Union type request/response
- Location:
apps/backend/src/api/rest/todos.rs:21
Request types (defined in models/api.rs):
pub enum TodoRequest {
List,
Get { id: String },
Create { title: String, description: Option<String> },
Update { id: String, title: Option<String>, ... },
Delete { id: String },
}Response types:
pub enum TodoResponse {
List { todos: Vec<ApiTodo> },
Todo { todo: ApiTodo },
Deleted { id: String },
}Why RPC-style?: Simpler client code, better type safety, single endpoint for all operations.
- Endpoint:
ws://localhost:8888/ws - Pattern: Broadcast to all connected clients
- Location:
apps/backend/src/api/ws/handler.rs:45
Architecture:
- Client connects → receives all current todos
- Client sends message → server processes → broadcasts to ALL clients
- Uses
tokio::sync::broadcastchannel for pub/sub
Client message types (defined in models/api.rs - WebSocket section):
pub enum ClientMessage {
Create { title: String, description: Option<String> },
Update { id: String, ... },
Delete { id: String },
Toggle { id: String },
}Server message types:
pub enum ServerMessage {
Connected { client_id: String, todos: Vec<ApiTodo> },
Created { todo: ApiTodo },
Updated { todo: ApiTodo },
Deleted { id: String },
Error { message: String },
}Key Implementation Detail: Global state using lazy_static + Arc<RwLock<WsState>> at handler.rs:40-42
The backend uses a 3-layer architecture for data handling:
-
Domain Layer (
models/domain.rs): Core data structures- Structs with
#[derive(sqlx::FromRow)] - Represents database records exactly
- Example:
TodowithUuid,DateTime<Utc>, etc.
- Structs with
-
Database Layer (
db/): Database operationsdb/mod.rs: MainDbstruct and connectiondb/todos.rs: Query implementations asimpl Dbmethodsdb/pg/mod.rs: PostgreSQL pool setup- Uses
sqlx::query_as!for compile-time verification
-
API Layer (
models/api.rs): Wire format types- Request/Response types for REST API
- WebSocket message types (
ClientMessage,ServerMessage) - Conversion implementations (
From<domain::Todo> for ApiTodo) - UUID as
String, dates serialized for JSON
Flow: Database → Domain models → API types → JSON
PostgreSQL with SQLx:
- Compile-time query verification
- Offline mode support (
.sqlx/directory) - Migration files named:
YYYYMMDDHHMMSS_description.sql
Database operations (apps/backend/src/db/todos.rs):
get_all_todos()- Fetch all todosget_todo_by_id()- Fetch single todocreate_todo()- Create new todoupdate_todo()- Update todo (partial updates supported)delete_todo()- Delete todo
Important: All queries use sqlx::query_as! macro for compile-time verification. Operations are implemented as methods on the Db struct.
apps/frontend/src/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout
│ └── page.tsx # Home page
├── components/
│ └── ui/ # shadcn/ui components
├── lib/ # Utilities
└── store/ # Zustand state management
- Define Rust types with
#[derive(ToSchema)]fromutoipa - Run:
just types(orcargo run --bin generate_openapi) - Generates:
packages/shared/openapi.json - SDK generation:
bun --filter @rust-react-starter/sdk generate - Creates:
packages/sdk/typescript/src/types/
Location: Type generation binary at apps/backend/src/bin/generate_openapi.rs
just setup # First-time setup (checks tools + installs + builds)
just db # Start database + run migrations
just db-prepare # Generate .sqlx/ for offline mode (commit to git!)
just types # Regenerate OpenAPI + TypeScript types
just backend # Start backend (http://localhost:8888)
just frontend # Start frontend (http://localhost:3000)just db # Start DB + run migrations
just db-stop # Stop DB (data preserved)
just db-reset # Destroy all data + fresh start
just db-migrate # Run migrations only
just db-prepare # Generate SQLx offline datajust build # Build all (uses SQLX_OFFLINE=true)
just test # Run all tests
just fmt # Format Rust + TypeScript
just lint # Lint all code
just typecheck # TypeScript type checking
just ci # Run all CI checks-
Define models in
apps/backend/src/models/api.rs:#[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct MyRequest { /* ... */ }
-
Create handler in
apps/backend/src/api/rest/:#[utoipa::path(post, path = "/api/my-endpoint", ...)] pub async fn handler(...) { /* ... */ }
-
Register route in
apps/backend/src/api/rest/mod.rs:.route("/api/my-endpoint", post(handler))
-
Add to OpenAPI in
mod.rs:#[openapi( paths(my_handler), components(schemas(MyRequest, MyResponse)) )]
-
Regenerate types:
just types
Create new migration:
# Create file: apps/backend/src/db/pg/migrations/YYYYMMDDHHMMSS_description.sql
# Run: just db-migrate
# Prepare: just db-prepare (commit .sqlx/ to git)Migration naming: Use timestamp format YYYYMMDDHHMMSS_description.sql
Example: 20250101000000_init.sql
- Why: Build without database connection (for CI/CD)
- Setup: Run
just db-prepareafter schema changes - Commit:
.sqlx/directory to git - Build: Uses
SQLX_OFFLINE=trueautomatically
Backend uses custom error types (apps/backend/src/errors.rs):
pub enum AppError {
BadRequest(String),
NotFound(String),
DatabaseError(String),
InternalError(String),
}Implements IntoResponse for automatic HTTP status codes.
- Create migration:
apps/backend/src/db/pg/migrations/YYYYMMDDHHMMSS_add_users.sql - Create domain model: Add to
apps/backend/src/models/domain.rs(struct withsqlx::FromRow) - Create DB operations:
apps/backend/src/db/users.rs(impl methods onDb) - Create API models: Add to
apps/backend/src/models/api.rs(request/response types) - Create handlers:
apps/backend/src/api/rest/users.rs - Register routes:
apps/backend/src/api/rest/mod.rs - Run:
just db-migrate && just db-prepare && just types
cd apps/frontend
npx shadcn@latest add [component-name]Components install to: apps/frontend/src/components/ui/
# Check migrations
docker exec -it rust-react-starter psql -U postgres -d rust-react-starter -c "\dt"
# View migration history
docker exec -it rust-react-starter psql -U postgres -d rust-react-starter -c "SELECT * FROM _sqlx_migrations"
# Reset if corrupted
just db-resetLocated in: apps/backend/tests/
Uses Testcontainers for isolated PostgreSQL instances.
Run: cargo test --test todos_test
Required in .env:
DATABASE_URL=postgresql://postgres:password@localhost:5432/rust-react-starter
HOST=localhost
PORT=8888
NEXT_PUBLIC_API_URL=http://localhost:8888Note: justfile exports DATABASE_URL automatically.
GitHub Actions workflow: .github/workflows/ci.yml
Runs:
- Backend: format, clippy, tests, build
- Frontend: typecheck, lint, build
- Integration: Full test suite with PostgreSQL
| File | Purpose |
|---|---|
justfile |
Task runner (like Makefile) |
Cargo.toml |
Rust workspace config |
package.json |
Bun workspace config |
apps/backend/src/main.rs:38 |
Server setup & routing |
apps/backend/src/api/rest/mod.rs:40 |
REST API router |
apps/backend/src/api/ws/handler.rs:45 |
WebSocket handler |
apps/backend/src/db/pg/todos.rs |
Database operations |
packages/shared/openapi.json |
Generated OpenAPI spec |
packages/sdk/typescript/src/types/ |
Generated TS types |
- WebSocket: Uses broadcast channels, efficient for multiple clients
- SQLx: Compile-time query verification prevents runtime errors
- Database: Indexes on
created_atandcompletedfor todos
- CORS: Permissive mode (change for production in
main.rs:42) - SQL Injection: Protected by SQLx parameterized queries
- Input Validation: Add validation in handlers (see
todos.rs:45-47)
just db-reset# Ensure migration file is named correctly: YYYYMMDDHHMMSS_description.sql
just db-migrate
just db-preparejust check-tools # Shows what's missing + installation links# Ensure .sqlx/ directory exists and is up to date
just db-prepare- Always run
just typesafter changing Rust models - Commit
.sqlx/directory afterjust db-prepare - Use RPC-style for simple CRUD, WebSocket for real-time
- Run
just cibefore committing - Name migrations with timestamp prefix
- Use
#[derive(ToSchema)]for OpenAPI generation - Keep domain models separate from API models
- Axum docs: https://docs.rs/axum
- SQLx docs: https://docs.rs/sqlx
- utoipa docs: https://docs.rs/utoipa
- Next.js docs: https://nextjs.org/docs
- shadcn/ui: https://ui.shadcn.com