Declare the entity
You declare the shape of a post once — a SeaORM model decorated with
#[expose] — and the macro emits the wire DTO, the create/update
inputs, the validator wiring, and the JSON schema the HTTP transport
reaches for. By the end of this page, nestrs run check passes on a new
posts feature crate path.
Move into the features crate
Section titled “Move into the features crate”The reference feature lives at crates/features/src/posts/. Create the
same folder so the tutorial can extend it in place.
Directorycrates/features/src/posts/
- mod.rs
- module.rs
- entity.rs
- service.rs
The posts module ships under the workspace’s features crate. Add the
dependencies the entity reaches for to crates/features/Cargo.toml —
each rides the version pinned in the workspace root:
[dependencies]nest-rs-resource.workspace = truenest-rs-seaorm.workspace = truesea-orm.workspace = trueserde.workspace = trueuuid.workspace = truevalidator.workspace = trueThe first cut
Section titled “The first cut”A post has an id, a title, and a body. SeaORM owns the column types;
#[expose] decides which columns cross the wire and which inputs the
HTTP layer accepts. Exposure is opt-in — a field surfaces only when it
carries #[expose]. No org column, no auth hooks — the tutorial keeps
the domain small so you can focus on the entity → service → controller
chain.
The crate this page builds on top of is
nest-rs-resource —
it provides the #[expose] macro and the WireModelDefaults trait the
masking layer reads when a feature later adds authorization. SeaORM
itself is the source of truth for entities, columns, and migrations.
use nest_rs_resource::expose;use sea_orm::entity::prelude::*;use serde::{Deserialize, Serialize};
#[expose(name = "Post", service = super::service::PostsService)]#[sea_orm::model]#[derive(Clone, Debug, DeriveEntityModel)]#[sea_orm( table_name = "post", model_attrs(derive(PartialEq, Serialize, Deserialize)))]pub struct Model { #[sea_orm(primary_key, auto_increment = false)] #[expose] pub id: Uuid, #[expose(input(create, update), validate(length(min = 1)))] pub title: String, #[expose(input(create, update), validate(length(min = 1)))] pub body: String,}
impl ActiveModelBehavior for ActiveModel {}What #[expose] decides
Section titled “What #[expose] decides”The decorator translates one declaration into every wire surface:
| Generated | What it is | Where it surfaces |
|---|---|---|
Post | The wire DTO — every #[expose]d column, no SeaORM internals and nothing left unexposed | HTTP responses, OpenAPI schema |
CreatePostDto | A struct holding fields marked input(create) | POST /posts body |
UpdatePostDto | A struct holding fields marked input(update) | PATCH /posts/:id body |
service = super::service::PostsService tells the macro which service
owns the entity. The path is required as soon as the entity declares an
exposed relation or another feature needs the PK loader — posts has
neither yet, but the attribute is already in place for when you add one.
Read the field-level attributes
Section titled “Read the field-level attributes”Two patterns cover this entity; a third shows up when you copy
users in api.
| Attribute | Effect |
|---|---|
#[expose] | Bare — the field is read-only on the wire (appears in Post), but not in any input |
#[expose(input(create, update), validate(length(min = 1)))] | Field appears in Post (input implies read) and in CreatePostDto and UpdatePostDto, with the validator rule wired |
#[expose(input(create))] | Field appears in Post and only in CreatePostDto — common for fields that can be set but not edited |
(no #[expose]) | Field is hidden from Post and from every input — the masker fills it back in from defaults so Ability::mask sees the whole row |
Leaving a field unexposed matters once a feature carries server-side
columns — password hashes, tenant ids set from a JWT. Because exposure
is opt-in, those columns stay off the wire by default;
api is where unexposed columns and masking become
load-bearing.
The mod entry
Section titled “The mod entry”The feature root re-exports what other modules import. Keep it small — this is the public surface.
mod entity;mod module;
pub use entity::*;pub use module::PostsModule;PostsService doesn’t exist yet; that’s the next page. For now, keep
module.rs and service.rs stubbed so the crate compiles.
use nest_rs_core::module;
#[module]pub struct PostsModule;// Filled in on the next page.Wire the feature into the parent crate’s lib.rs:
pub mod posts;$ nestrs run check Checking features v0.1.0 Finished `dev` profile [unoptimized] target(s) in 1.42sThe macro expanded, the entity compiled, and Post / CreatePostDto
/ UpdatePostDto are now usable from the rest of the crate.
What you have now
Section titled “What you have now”- A
PostSeaORM entity withid,title,body. - An auto-generated
Postwire DTO andCreatePostDto/UpdatePostDtoinput structs. - A
PostsModuleshell ready to declare its first provider.