Full-stack validation ecosystem for Rust web services
Define validation once. Get runtime checks, OpenAPI schemas, TypeScript types, browser validation via WASM, and framework integration—from one source of truth.
Rust Domain Frontend
| |
#[derive(Validate, ToSchema)] domainstack zod
| |
v v
.validate()? Zod schemas
| |
v v
Axum / Actix / Rocket <------> Same rules,
| both sides
v
Structured errors (field-level, indexed paths)
Progressive adoption — use what you need:
| Need | Start With |
|---|---|
| Just validation | domainstack core |
| + derive macros | + domainstack-derive |
| + OpenAPI schemas | + domainstack-schema |
| + Axum/Actix/Rocket | + framework adapter |
| + TypeScript/Zod/JSON Schema | + domainstack-cli |
| + Browser (WASM) | + domainstack-wasm |
domainstack turns untrusted input into valid-by-construction domain objects with stable, field-level errors your APIs and UIs can depend on.
Built for the boundary you actually live at:
HTTP/JSON → DTOs → Domain (validated) → Business logic
Gate 1: Serde (decode + shape) — JSON → DTO. Fails on invalid JSON, type mismatches, missing required fields.
Gate 2: Domain (construct + validate) — DTO → Domain. Produces structured field-level errors your APIs depend on.
After domain validation succeeds, you can optionally run async/context validation (DB/API checks like uniqueness, rate limits, authorization) as a post-validation phase.
validator and garde focus on: "Is this struct valid?"
domainstack focuses on the full ecosystem:
- DTO → Domain conversion with field-level error paths
- Rules as composable values (
.and(),.or(),.when()) - Async validation with context (DB checks, API calls)
- Framework adapters that map errors to structured HTTP responses
- OpenAPI schemas generated from the same validation rules
- TypeScript/Zod codegen for frontend parity
If you want valid-by-construction domain types with errors that map cleanly to forms and clients, domainstack is purpose-built for that.
- Domain-first modeling: make invalid states difficult (or impossible) to represent
- Composable rule algebra: reusable rules with
.and(),.or(),.when() - Structured error paths:
rooms[0].adults,guest.email.value(UI-friendly) - Cross-field validation: invariants like password confirmation, date ranges
- Type-state tracking: phantom types to enforce "validated" at compile time
- Schema + client parity: generate OpenAPI (and TypeScript/Zod via CLI) from the same Rust rules
- Framework adapters: one-line boundary extraction (Axum / Actix / Rocket)
- Lean core: zero-deps base, opt-in features for regex / async / chrono / serde
- Quick Start
- Installation
- Key Features
- Examples
- Framework Adapters
- Running Examples
- Crates
- Documentation
- License
- Contributing
Dependencies (add to Cargo.toml):
[dependencies]
domainstack = { version = "1.0", features = ["derive", "regex", "chrono"] }
domainstack-derive = "1.0"
domainstack-axum = "1.0" # Or domainstack-actix if using Actix-web
serde = { version = "1", features = ["derive"] }
chrono = "0.4"
axum = "0.7"Complete example (with all imports):
use axum::Json;
use chrono::NaiveDate;
use domainstack::prelude::*;
use domainstack_axum::{DomainJson, ErrorResponse};
use domainstack_derive::{ToSchema, Validate};
use serde::Deserialize;
// DTO from HTTP/JSON (untrusted input)
#[derive(Deserialize)]
struct BookingDto {
guest_email: String,
check_in: String,
check_out: String,
rooms: Vec<RoomDto>,
}
#[derive(Deserialize)]
struct RoomDto {
adults: u8,
children: u8,
}
// Domain models with validation rules (invalid states impossible)
#[derive(Validate, ToSchema)]
#[validate(
check = "self.check_in < self.check_out",
message = "Check-out must be after check-in"
)]
struct Booking {
#[validate(email, max_len = 255)]
guest_email: String,
check_in: NaiveDate,
check_out: NaiveDate,
#[validate(min_items = 1, max_items = 5)]
#[validate(each(nested))]
rooms: Vec<Room>,
}
#[derive(Validate, ToSchema)]
struct Room {
#[validate(range(min = 1, max = 4))]
adults: u8,
#[validate(range(min = 0, max = 3))]
children: u8,
}
// TryFrom: DTO → Domain conversion with validation
impl TryFrom<BookingDto> for Booking {
type Error = ValidationError;
fn try_from(dto: BookingDto) -> Result<Self, Self::Error> {
let booking = Self {
guest_email: dto.guest_email,
check_in: NaiveDate::parse_from_str(&dto.check_in, "%Y-%m-%d")
.map_err(|_| ValidationError::single("check_in", "invalid_date", "Invalid date format"))?,
check_out: NaiveDate::parse_from_str(&dto.check_out, "%Y-%m-%d")
.map_err(|_| ValidationError::single("check_out", "invalid_date", "Invalid date format"))?,
rooms: dto.rooms.into_iter().map(|r| Room {
adults: r.adults,
children: r.children,
}).collect(),
};
booking.validate()?; // Validates all fields + cross-field rules!
Ok(booking)
}
}
// Axum handler: one-line extraction with automatic validation
type BookingJson = DomainJson<Booking, BookingDto>;
async fn create_booking(
BookingJson { domain: booking, .. }: BookingJson
) -> Result<Json<Booking>, ErrorResponse> {
// booking is GUARANTEED valid here - use with confidence!
save_to_db(booking).await
}On validation failure, automatic structured errors:
{
"status": 400,
"message": "Validation failed with 3 errors",
"details": {
"fields": {
"guest_email": [
{"code": "invalid_email", "message": "Invalid email format"}
],
"rooms[0].adults": [
{"code": "out_of_range", "message": "Must be between 1 and 4"}
],
"rooms[1].children": [
{"code": "out_of_range", "message": "Must be between 0 and 3"}
]
}
}
}Auto-generated TypeScript/Zod from the same Rust code:
// Zero duplication - generated from Rust validation rules!
export const bookingSchema = z.object({
guest_email: z.string().email().max(255),
check_in: z.string(),
check_out: z.string(),
rooms: z.array(z.object({
adults: z.number().min(1).max(4),
children: z.number().min(0).max(3)
})).min(1).max(5)
}).refine(
(data) => data.check_in < data.check_out,
{ message: "Check-out must be after check-in" }
);Or run the same Rust validation in the browser via WASM:
import init, { createValidator } from '@domainstack/wasm';
await init();
const validator = createValidator();
const result = validator.validate('Booking', JSON.stringify(formData));
if (!result.ok) {
result.errors.forEach(e => setFieldError(e.path, e.message));
}Server and browser return identical error structures (paths, codes, metadata)—UI rendering logic works unchanged. → WASM Guide
[dependencies]
domainstack = { version = "1.0", features = ["derive", "regex"] }
domainstack-axum = "1.0" # or domainstack-actix, domainstack-rocket
# Optional
domainstack-schema = "1.0" # OpenAPI generationFor complete installation guide, feature flags, and companion crates, see INSTALLATION.md
- 37 Validation Rules - String, numeric, collection, and date/time validation → RULES.md
- Derive Macros -
#[derive(Validate)]for declarative validation → DERIVE_MACRO.md - Composable Rules -
.and(),.or(),.when()combinators for complex logic → RULES.md - Nested Validation - Automatic path tracking for deeply nested structures → DERIVE_MACRO.md
- Collection Validation - Array indices in error paths (
items[0].field) → COLLECTION_VALIDATION.md - Serde Integration - Validate during deserialization → SERDE_INTEGRATION.md
- Async Validation - Database/API checks with
AsyncValidate→ ADVANCED_PATTERNS.md - Cross-Field Validation - Password confirmation, date ranges → DERIVE_MACRO.md
- Type-State Validation - Compile-time guarantees with phantom types → ADVANCED_PATTERNS.md
- OpenAPI Schema Generation - Auto-generate schemas from rules → OPENAPI_SCHEMA.md
- JSON Schema Generation - Draft 2020-12 schemas via trait or CLI → JSON_SCHEMA.md
- Framework Adapters - Axum, Actix-web, Rocket extractors → HTTP_INTEGRATION.md
- WASM Browser Validation - Same Rust code runs in browser via WebAssembly → WASM_VALIDATION.md
Most validation is declarative with #[derive(Validate)]:
#[derive(Debug, Validate)]
struct User {
#[validate(length(min = 3, max = 20))]
#[validate(alphanumeric)]
username: String,
#[validate(email)]
#[validate(max_len = 255)]
email: String,
#[validate(range(min = 18, max = 120))]
age: u8,
}
// Validate all fields at once
let user = User { username, email, age };
user.validate()?; // [ok] Validates all constraintsSee examples/ folder for more:
- Nested validation with path tracking
- Collection item validation
- Manual validation for newtypes
- Cross-field validation
- Async validation
- Type-state patterns
- OpenAPI schema generation
- Framework integration examples
One-line DTO→Domain extractors with automatic validation and structured error responses:
| Framework | Crate | Extractor |
|---|---|---|
| Axum | domainstack-axum |
DomainJson<T, Dto> |
| Actix-web | domainstack-actix |
DomainJson<T, Dto> |
| Rocket | domainstack-rocket |
DomainJson<T, Dto> |
See examples/README.md for instructions on running all examples.
9 publishable crates for modular adoption:
| Category | Crates |
|---|---|
| Core | domainstack, domainstack-derive, domainstack-schema, domainstack-envelope |
| Web Framework Integrations | domainstack-axum, domainstack-actix, domainstack-rocket, domainstack-http |
| Browser | domainstack-wasm — Same validation in browser via WebAssembly |
4 example crates (repository only): domainstack-examples, examples-axum, examples-actix, examples-rocket
📦 Complete Crate List - Detailed descriptions and links
| Getting Started | |
| Core Concepts | Valid-by-construction types, error paths |
| Domain Modeling | DTO→Domain, smart constructors |
| Installation | Feature flags, companion crates |
| Guides | |
| Derive Macro | #[derive(Validate)] reference |
| Validation Rules | All 37 built-in rules |
| Error Handling | Violations, paths, i18n |
| HTTP Integration | Axum / Actix / Rocket |
| Advanced | |
| Async Validation | DB/API checks, context |
| OpenAPI Schema | Trait-based generation |
| JSON Schema | Trait-based generation |
| CLI Codegen | Zod, JSON Schema, OpenAPI |
| Serde Integration | Validate on deserialize |
| WASM Browser | Same validation in browser |
Reference: API Docs · Architecture · Examples · Publishing
MIT OR Apache-2.0
Dayna Blackwell - blackwellsystems@protonmail.com
This is an early-stage project. Issues and pull requests are welcome!