Skip to content

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.

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:

crates/features/Cargo.toml
[dependencies]
nest-rs-resource.workspace = true
nest-rs-seaorm.workspace = true
sea-orm.workspace = true
serde.workspace = true
uuid.workspace = true
validator.workspace = true

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.

crates/features/src/posts/entity.rs
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 {}

The decorator translates one declaration into every wire surface:

GeneratedWhat it isWhere it surfaces
PostThe wire DTO — every #[expose]d column, no SeaORM internals and nothing left unexposedHTTP responses, OpenAPI schema
CreatePostDtoA struct holding fields marked input(create)POST /posts body
UpdatePostDtoA 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.

Two patterns cover this entity; a third shows up when you copy users in api.

AttributeEffect
#[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 feature root re-exports what other modules import. Keep it small — this is the public surface.

crates/features/src/posts/mod.rs
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.

crates/features/src/posts/module.rs
use nest_rs_core::module;
#[module]
pub struct PostsModule;
crates/features/src/posts/service.rs
// Filled in on the next page.

Wire the feature into the parent crate’s lib.rs:

crates/features/src/lib.rs
pub mod posts;
Terminal window
$ nestrs run check
Checking features v0.1.0
Finished `dev` profile [unoptimized] target(s) in 1.42s

The macro expanded, the entity compiled, and Post / CreatePostDto / UpdatePostDto are now usable from the rest of the crate.

  • A Post SeaORM entity with id, title, body.
  • An auto-generated Post wire DTO and CreatePostDto / UpdatePostDto input structs.
  • A PostsModule shell ready to declare its first provider.

Next: write the service and expose it over HTTP →