Rust hexagonal
List of custom agent skills
npx -y skills add pedromneto97/custom-skills --skill rust-hexagonalAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Hexagonal architecture (Ports & Adapters) for Rust backends. Use when: creating a new Rust backend, structuring a domain layer, defining ports as traits, implementing adapters (HTTP, DB, gRPC), wiring dependency injection, separating business logic from infrastructure, applying clean architecture in Rust, organizing modules for testability.
SKILL.md
9.8 KB, as published. Nobody here has run it
Hexagonal Architecture for Rust Backends
Overview
Hexagonal Architecture (Ports & Adapters) isolates the domain from all external concerns. Nothing in domain or use cases ever imports an infrastructure crate.
Call flow (always this direction):
[ HTTP / CLI / gRPC ] → inbound → use case fns(ports...) → domain → port traits → outbound → [ DB / APIs / cache / email ]
Dependencies point inward only (enforced via Cargo.toml):
app → inbound ─┐
├→ domain (model + errors + ports + use cases)
app → outbound ─┘
Typical workspace shape is four crates + one binary:
domain— entities, value objects, errors, ports, use cases.inbound— HTTP/gRPC/CLI adapters; depend only ondomain.outbound— DB/API/cache/email adapters; depend only ondomain.migration(ormigrations) — schema migrations only.app— binary; the only crate that knows all concrete types.
Is this still hexagonal? Yes. The invariant holds: domain/use cases never depend on infrastructure.
Merge use cases into domain, or keep a separate application crate?
| Layout A — merge (recommended) | Layout B — separate application | |
|---|---|---|
| Crate count | 4 | 5 |
domain deps | may include tokio | stays fully dep-free |
| Best for | most projects | shared domain across multiple services or publishing domain as a lib |
async-traitrule: With Rust ≥ 1.75 and static dispatch, nativeasync fnin traits is sufficient — noasync-traitneeded. Only add it if you explicitly needdyn Traitobject safety.
Companion Skills
Load these skills when working on the corresponding layers:
| Layer | Skill | Covers |
|---|---|---|
| Inbound (HTTP) | http-actix-axum | REST naming, status codes, RFC 9457 Problem Details, CORS, OWASP security headers, compression, versioning |
| Outbound (DB) | sea-orm | Migrations, entity generation, CRUD, relations, pagination, on-conflict, SeaORM v2 macros |
These skills are authoritative for their layer. Prefer them over inline examples when detail is needed.
Workspace Structure
my-app/
├── Cargo.toml # workspace root
├── domain/ # lib — model, errors, ports, use cases (Layout A)
├── inbound/ # lib — HTTP/gRPC/CLI adapters (actix-web preferred; axum supported)
├── outbound/ # lib — DB/cache adapters (SeaORM v2)
├── migration/ # lib — DB migrations (some teams prefer name: migrations)
└── app/ # bin — sole composition root
Layout B adds an application/ crate between domain and app; domain then stays dep-free.
See folder structure & Cargo.toml templates.
Key Rules
| Rule | Why |
|---|---|
domain never imports infrastructure crates | Portable, testable core |
Port traits live in domain | Domain owns its own contracts |
Use cases are free async functions generic over R: Repository | Static dispatch, no boilerplate trait impl |
Domain ports are split by responsibility (ports/repository/*, ports/service/*) | Clarifies persistence vs external services contracts |
| Inbound handlers remain generic over traits, not concrete structs | No concrete adapter type leaks into the HTTP layer |
Inbound DI can use one AppState<...> or multiple typed app-data values | Supports both simple and multi-adapter systems |
inbound and outbound depend only on domain | No cross-adapter coupling |
Prefer impl Iterator<Item = T> over Vec<T> in collection ports | Caller decides whether/how to allocate |
Prefer static dispatch (generics); use Arc<dyn Trait> only when runtime polymorphism is needed | Zero-cost unless you opt in |
No async-trait unless you need dyn Trait object safety | Native async fn in traits (Rust ≥ 1.75) |
app/main.rs is the only file importing all concrete types | Single composition root |
Repository supertraits may live in domain/ports/mod.rs when many traits are combined | Keeps combined bounds where both inbound and outbound can use them |
Scaffold Checklist
Load only the reference for the layer you're working on:
| Step | What | Reference |
|---|---|---|
| 1 | Domain model (entities, value objects) | domain.md |
| 2 | Domain errors | domain.md |
| 3 | Outbound port (repository trait) | domain.md |
| 4 | Use case functions | domain.md |
| 5 | DB migrations | migrations.md |
| 6 | Outbound adapter (SeaORM repository + mappers) | outbound.md + sea-orm skill |
| 7 | Inbound adapter (actix-web / axum + extractor patterns) | inbound.md + http-actix-axum skill |
| 8 | Wire & run | bootstrap.md |
Testing Strategy
| Layer | Approach |
|---|---|
| Domain model | Pure unit tests — no async, no mocks |
| Use case | #[tokio::test] + mockall mock of outbound port |
| Outbound adapter | SeaORM MockDatabase or test containers |
| Inbound adapter | actix_web::test with generic handler + mock repository |
See testing reference.
Recommended Crates
| Purpose | Crate |
|---|---|
| HTTP framework | actix-web (preferred) or axum |
| Async runtime | tokio |
| Database ORM | sea-orm v2 |
| Error handling | thiserror + anyhow |
| Async traits (opt-in) | async-trait (only if dyn Trait needed) |
| Mocking | mockall ≥ 0.12 |
| Serialization | serde + serde_json |
| Validation | validator |
| Tracing | tracing + tracing-subscriber |
| Config / env | dotenvy + config |
Common Pitfalls
- Domain importing infrastructure crates —
domain/Cargo.tomlmust list nosea-orm,actix-web,axum, etc. - Cross-adapter coupling —
inboundandoutboundmust not import each other; enforced by theirCargo.toml. - Handler holding a concrete service type —
web::Data<OrderService<PgRepo>>leaks the concrete type. Keep handlers generic over traits and inject concrete types only inapp. - Unnecessary
async-trait— only add it when you needdyn Trait. With static dispatch and Rust ≥ 1.75, remove it from port traits. mockall::automockon RPIT traits —#[automock]compile-errors when a trait method returnsimpl Traitin return position (e.g.async fn find_all() -> Result<impl Iterator<Item = T>, _>). Only annotate traits with concrete return types. For RPIT traits write a hand-written fake struct in a#[cfg(test)]block — see testing reference.- Returning
Vec<T>from collection ports — preferimpl Iterator<Item = T>; the adapter collects from DB internally but the interface stays flexible. - Leaking SeaORM types into domain —
Model,ActiveModel, column enums belong inoutbound/src/db/. Map to domain structs inmappers.rsimmediately. - Bloated
app/main.rs— extractfn wire_orders(db)helpers per bounded context; keepmainunder ~30 lines. - Using
.insert()insave()— always INSERT fails for existing records. Use anON CONFLICT … DO UPDATEupsert (seeoutbound.md). find_all()without pagination — fetching entire tables into memory will OOM in production. Addpage/limitor cursor parameters to collection port signatures before the table grows.- Expanding
AppRepositorywith every new trait — when adding a second bounded context (e.g.CustomerRepository), create a focusedCustomerAppRepositorysupertrait rather than bolting it onto the existing one. Handlers that only handle orders should not receive a repo that also exposes customer operations. - Treating DI shape as architecture law — hexagonal architecture does not require one
AppStateformat. Prefer the shape that keeps adapter boundaries clear for your runtime and framework.
Scaling to Multiple Bounded Contexts
As the application grows beyond a single orders domain, organise by bounded context within each crate before splitting crates:
domain/src/
├── lib.rs # pub mod orders; pub mod customers;
├── orders/
│ ├── mod.rs # pub mod model; pub mod error; pub mod ports; pub mod use_cases;
│ └── …
└── customers/
├── mod.rs
└── …
inbound/src/
├── state.rs # separate AppRepository supertrait per context if needed
└── http/
├── orders/ # handlers, router, dto for orders
└── customers/ # handlers, router, dto for customers
outbound/src/db/
├── orders/ # repository, entities, mappers
└── customers/
When to split into separate crates / microservices:
| Signal | Action |
|---|---|
| Two bounded contexts never share a DB transaction | Safe to extract into separate services |
| Compile times become painful | Move large bounded context to its own workspace |
| Independent deployment cadence required | Extract to its own binary (app-orders, app-customers) |
| Team ownership boundaries diverge | Separate repos with a shared domain lib published to a registry |