Skip to main content
Background Image
  1. Projects/

Stilltypes - Domain-Specific Refined Types for Rust

Author
Glen Baker
Building tech startups and open source tooling
Table of Contents

Domain types that prove validity - validate once, trust everywhere

Crates.io
Documentation
GitHub

Links: GitHub | Crates.io | Documentation

Overview
#

Stilltypes provides production-ready refined types for common domain concepts. Validate emails, URLs, phone numbers, IBANs, and more with types that prove validity at construction time. Once you have an Email, you know it’s valid - no re-validation needed throughout your codebase.

The Problem
#

Domain validation in Rust often suffers from:

  • Stringly-typed code - Passing String everywhere means any function could receive invalid data
  • Repeated validation - The same validation logic scattered across endpoints, services, and models
  • Poor error messages - Generic “invalid input” errors that don’t help users fix their data
  • Validation drift - Different parts of the codebase enforcing different rules for the same concept
  • Missing edge cases - Hand-rolled regex missing RFC edge cases for emails, URLs, phone numbers

The Solution
#

Stilltypes provides refined types that validate on construction and carry proof of validity in the type:

use stilltypes::prelude::*;

// Types validate on construction
let email = Email::new("[email protected]".to_string())?;
let url = SecureUrl::new("https://example.com".to_string())?;
let phone = PhoneNumber::new("+1 415 555 1234".to_string())?;

// Invalid values fail with helpful errors
let bad = Email::new("invalid".to_string());
// Error: invalid email address: invalid format, expected local@domain

Once constructed, these types flow through your codebase with compile-time guarantees.

Key Features
#

Modular Dependencies
#

Enable only what you need - each domain type has its own feature flag:

[dependencies]
stilltypes = { version = "0.2", default-features = false, features = ["email", "url"] }
FeatureTypesExternal Deps
email (default)Emailemail_address
url (default)Url, HttpUrl, SecureUrlurl
uuidUuid, UuidV4, UuidV7uuid
phonePhoneNumberphonenumber
financialIban, CreditCardNumberiban_validate, creditcard
networkIpv4Addr, Ipv6Addr, Port, DomainName-
geoLatitude, Longitude-
numericPercentage, UnitInterval-
identifiersSlug-

Helpful Error Messages
#

Errors tell users how to fix their input:

let result = Email::new("missing-at-sign".to_string());
// Error: invalid email address: invalid format, expected local@domain (example: [email protected])

let result = Port::new(70000);
// Error: port must be between 0 and 65535

Semantic Helpers
#

Types include domain-appropriate helper methods:

// IP address classification
let ip = Ipv4Addr::new("192.168.1.1".to_string())?;
assert!(ip.is_private());
assert!(!ip.is_loopback());

// Port range classification
let port = Port::new(443)?;
assert!(port.is_privileged());
assert!(port.is_well_known());

// Phone number normalization
let phone = PhoneNumber::new("+1 (415) 555-1234".to_string())?;
assert_eq!(phone.to_e164(), "+14155551234");

// Coordinate conversion
let lat = Latitude::new(37.7749)?;
let (deg, min, sec, hemi) = lat.to_dms();  // 37° 46' 29.64" N

Serde Integration
#

With the serde feature, types validate during deserialization:

use stilltypes::prelude::*;
use serde::Deserialize;

#[derive(Deserialize)]
struct UserInput {
    email: Email,
    website: Option<SecureUrl>,
}

// Invalid JSON fails to deserialize with validation error
let result: Result<UserInput, _> = serde_json::from_str(json);

Financial Types
#

Specialized support for financial identifiers:

use stilltypes::financial::{Iban, CreditCardNumber, IbanExt, CreditCardExt};

// IBAN with country code extraction
let iban = Iban::new("DE89370400440532013000".to_string())?;
assert_eq!(iban.country_code(), "DE");
assert_eq!(iban.masked(), "DE89****3000");  // Safe for display

// Credit card with Luhn validation
let card = CreditCardNumber::new("4111111111111111".to_string())?;
assert_eq!(card.masked(), "****1111");
assert_eq!(card.last_four(), "1111");

No Effects Required
#

Stilltypes uses stillwater’s Refined<T, P> type internally, but your code doesn’t need to use stillwater’s effect system. Types work with standard Result:

use stilltypes::email::Email;

// Just returns Result<Email, RefinementError>
fn register_user(email_str: &str) -> Result<User, MyError> {
    let email = Email::new(email_str.to_string())?;  // Standard ? operator
    db.insert_user(email.value())
}

Technical Highlights
#

  • Language: Rust
  • Core Pattern: Refined types with predicates
  • Validation: RFC-compliant where applicable (emails, URLs, phone numbers)
  • Dependencies: Modular - only pull in what you use
  • Serde: Optional serialization support
  • Ecosystem: Part of the Stillwater family

Installation
#

# Default (email + url)
cargo add stilltypes

# Specific features
cargo add stilltypes --features uuid,phone

# Everything
cargo add stilltypes --features full

# With serde support
cargo add stilltypes --features full,serde

Quick Example
#

use stilltypes::prelude::*;

// Form validation with type-safe fields
struct ValidatedForm {
    email: Email,
    website: SecureUrl,
    phone: PhoneNumber,
}

fn validate_form(
    email: String,
    website: String,
    phone: String,
) -> Result<ValidatedForm, Vec<String>> {
    let mut errors = vec![];

    let email = Email::new(email).map_err(|e| errors.push(e.to_string())).ok();
    let website = SecureUrl::new(website).map_err(|e| errors.push(e.to_string())).ok();
    let phone = PhoneNumber::new(phone).map_err(|e| errors.push(e.to_string())).ok();

    if errors.is_empty() {
        Ok(ValidatedForm {
            email: email.unwrap(),
            website: website.unwrap(),
            phone: phone.unwrap(),
        })
    } else {
        Err(errors)
    }
}

The Stillwater Ecosystem
#

LibraryPurpose
stillwaterEffect composition and validation core
stilltypesDomain-specific refined types
mindsetZero-cost state machines
premortemConfiguration validation
postmortemJSON validation with path tracking

Project Status
#

Active development, available on crates.io. Version 0.2.0 with stable APIs.

Links#

Related

Stillwater - Pure Core, Imperative Shell for Rust
Postmortem - Schema Validation for Rust
Premortem - Configuration Validation for Rust
Mindset - Zero-Cost State Machines for Rust
Premortem vs Figment: Configuration Libraries for Rust Applications
Debtmap - Rust Technical Debt Analyzer
Prodigy - AI Workflow Orchestration for Claude
Three Patterns That Made Prodigy's Functional Migration Worth It