Puerto

Getting Started

Puerto is a CLI tool that scaffolds production-ready Rust backends with Domain-Driven Design architecture. From install to running API in under a minute.

Prerequisites

  • Rust stable (1.85+) — install via rustup
  • Cargo (included with Rust)
  • Optional: PostgreSQL + SQLx CLI for database projects

Installation

Install the Puerto CLI from crates.io:

$ cargo install puerto

Verify the installation:

$ puerto --version

New Project

Create a new Puerto workspace. The interactive wizard asks for your project name, whether you need database support, and whether to include the Greeting demo entity.

$ puerto new my-app

For non-interactive use (CI/scripts):

$ puerto new --name my-app --db --no-demo

Generated structure:

my-app/
business/← domain + application
src/domain/greeting/
src/application/greeting/
infrastructure/← adapters
src/greeting/repository.rs
presentation/← http api
src/api/greeting/routes.rs
puerto.toml
Cargo.toml

Generate Scaffold

Scaffold a complete DDD entity across all layers. Puerto creates the domain model, repository trait, use cases, infrastructure adapter, and presentation routes — wired together automatically.

$ puerto generate scaffold User

What gets created:

business/model.rs, errors.rs, repository.rs (trait + mockall mock), 5 use cases (create, get, list, update, delete)
infrastructure/repository (InMemory or Pg — inferred from puerto.toml)
presentation/routes.rs, dto.rs, responses.rs, error_mapper.rs

Entity Fields

Define typed fields on your entities. The type system flows through every DDD layer — domain model, Params, DTOs, repository rows, and SQL migrations all derive from the field list.

From the CLI:

$ puerto generate scaffold Product -- name:String price:i64! sku:String

Fields go after -- (required separator). Append ! after a type to mark it as unique (e.g., sku:String! generates a unique DB constraint). Fields are persisted to puerto.toml and used by all generators.

Or edit puerto.toml directly:

[[entity]]
name = "Product"
use_cases = ["create_product", "list_products"]
[[entity.fields]]
name = "name"
type = "String"
[[entity.fields]]
name = "price"
type = "i64"
[[entity.fields]]
name = "sku"
type = "String"
unique = true

Supported types:

Rust Type SQL Type OpenAPI
StringTEXTstring
i64BIGINTinteger(int64)
boolBOOLEANboolean
f64DOUBLEnumber(double)
UuidUUIDstring(uuid)
DateTime<Utc>TIMESTAMPTZstring(date-time)
Option<T>nullableT?
Vec<String>TEXT[]array[string]
HashMap<String, String>JSONBobject

Every entity always includes id, created_at, updated_at, deleted, and deleted_at as system fields — these are generated automatically and should not be added to puerto.toml.

If entity.fields is empty or absent, entities default to a single name: String field for backward compatibility.

Validate your configuration:

$ puerto validate

Checks entity names (PascalCase), field names (snake_case), field types (against the type registry), duplicate entities and fields, and warns about Option<T> fields marked unique.

Value Objects

Wrap primitive fields in strongly-typed Value Objects. Puerto generates the VO structs, error variants, and all layer conversions automatically — domain, application, infrastructure, and presentation.

Field syntax

Format Result
name:Name:StringString VO wrapping a primitive
age:Age:i64Numeric VO (i64 / f64), pass-through
mid:Mid:opt:StringOption<Mid> — nullable VO
tags:Tag:vec:StringVec<Tag> — array VO
status:Status:enum:A/B/CEnum VO — from_str() / as_str()
sku:Sku:String!Unique VO (DB constraint)
email:EmailShared VO — type inferred from puerto.toml

Example

$ puerto generate scaffold Order -- amount:Amount:f64 status:Status:enum:Pending/Confirmed/Cancelled

What gets generated

domain/value_objects.rs with VO structs, Invalid* error variants, model using VO types
application/params use primitives; use case impl constructs VOs before calling Entity::new()
infrastructure/DB row uses primitives; TryFrom reconstructs VOs, From extracts them
presentation/DTO uses primitives; from_domain() calls .value() / .as_str()

Shared Value Objects

Declare reusable VOs once — any entity can reference them by name and the type is inferred automatically:

$ puerto generate value-object Email String
$ puerto generate scaffold User -- email:Email name:Name:String

Shared VOs are declared in puerto.toml as [[value_object]] entries and generated at business/src/domain/shared/value_objects.rs. Their import path switches to crate::domain::shared::value_objects automatically.

Validation

puerto validate checks VO names (PascalCase), inner types (allowed primitives), enum variants (PascalCase, non-empty), and rejects unique on Option<T> or Vec<T> VOs.

Layer Generators

Generate one DDD layer at a time. Ideal for AI-assisted development — keeping each agent focused on a single layer prevents context drift and makes compiler errors immediately actionable.

$ puerto generate domain Product
$ puerto generate application Product
$ puerto generate repository Product
$ puerto generate presentation Product
domainmodel, errors, repository trait, 5 use case traits, ProductMother (Object Mother) — registers entity in puerto.toml
application5 use case implementations with unit tests
repositoryInMemory or Pg adapter — inferred from puerto.toml
presentationroutes, dto, responses, error mapper — regenerates bootstrap.rs

Each command validates the prior step completed and prints a Next: hint. Use puerto generate scaffold when you want all layers at once.

Add a Use Case

Add a single use case to an existing entity. Puerto generates the trait, implementation, and unit tests.

$ puerto generate use-case User create

Database (SQLx)

Puerto has first-class SQLx + Postgres support. Enable it when creating a project — Puerto infers the choice for every subsequent scaffold automatically.

$ puerto new --name my-app --db
$ puerto generate scaffold User
$ puerto generate migration create_users_table

Puerto reads puerto.toml to decide: db projects get a PgUserRepository with a connection pool and auto-created migration; non-db projects get InMemoryUserRepository. No flag needed on every scaffold.

Architecture

Puerto enforces a strict dependency rule: dependencies only point inward. The domain layer has zero external dependencies.

dependency flow (inward only)
presentation/ → depends on infrastructure/
infrastructure/ → depends on business/
business/ → depends on nothing (std + thiserror only)

IDE Snippets

Every puerto new project ships with 28 Puerto-adapted Rust snippets for Zed, VS Code, and nvim+LuaSnip. No configuration needed — files are written automatically and loaded by your editor from the project root.

Zed

Snippets are written to .zed/snippets/rust.json. Zed loads project-local snippets automatically — no further setup.

VS Code

Snippets are written to .vscode/puerto.code-snippets. VS Code picks up workspace snippet files automatically.

nvim + LuaSnip

The VS Code file uses TextMate format, which LuaSnip can load directly. Add to your config:

require("luasnip.loaders.from_vscode").lazy_load({ paths = {"./.vscode"} })

Available snippets

Domain
domain-model Entity struct + Props + new(props) + from_repository()
domain-errors thiserror enum with machine-readable error codes
repository-trait RepositoryTrait + mockall mock in pub mod mocks
domain-use-case Params struct + UseCaseTrait
Value Objects
vo-string String VO — private field, trim + empty validation, value() → &str
vo-numeric Numeric VO (i64 / f64) — tuple struct, pass-through, value() by copy
vo-enum Enum VO — from_str() / as_str() pattern with variant tab stops
vo-option-construct Option<VO> construction — params.field.map(Vo::new).transpose()?
vo-vec-construct Vec<VO> construction — map + collect::<Result<Vec<_>, _>>()?
Application
app-use-case UseCaseImpl with LoggerTrait injection + unit tests included
Infrastructure
persistence-entity DB row struct (FromRow) + TryFrom/From conversions
persistence-repo PgEntityRepository — find_by_id + save with SQLx macros
Tests
should-do-test #[tokio::test] with Arrange / Act / Assert comments
sqlx-test #[sqlx::test(migrations = "migrations")] — real Postgres pool
sqlx-repo-test-module Full integration test module for PgEntityRepository
object-mother Object Mother builder: random(), with_name(), build()

+ 9 more: lib.rs blocks, poem DTOs, response enums, error mappers, API struct

Switching IDE or regenerating

Run puerto generate snippets from your project root at any time to write both IDE files. Use --ide to target a specific editor:

$ puerto generate snippets
$ puerto generate snippets --ide zed
$ puerto generate snippets --ide vscode