Validate inputs
You already wrote the validation rules on the entity — page 2. The
#[crud] macro’s generated POST /posts handler takes
Valid<Json<CreatePostDto>>, so every request body is checked against
those rules before PostsService::create runs. By the end of this page,
curl returns a structured 400 for an empty title and the service
never sees the bad input.
No handler edit required
Section titled “No handler edit required”On the previous page you left impl PostsController {} empty. That is
not a shortcut — for a bare CRUD feature, the macro is the handler.
#[crud] emits create and update methods that already wrap the
body in Valid<...>:
async fn create( &self, __body: ::nest_rs_http::Valid<::poem::web::Json<CreatePostDto>>,) -> ::poem::Result<::poem::web::Json<Post>> { // delegates to CrudService::create}When you later override create on a richer feature — org id from a JWT,
an explicit authz check — you keep the same extractor:
Valid<Json<CreatePostDto>>. The
users controller in api is the
reference override.
The crate driving this page is
nest-rs-pipes —
it provides the transport-agnostic Pipe trait and the bundled
ValidationPipe<T>. The HTTP binding lives in nest-rs-http as the
Valid<E> extractor.
Where the rules live
Section titled “Where the rules live”You already wrote the rules — on the entity, page 2:
#[expose(input(create, update), validate(length(min = 1)))]pub title: String,
#[expose(input(create, update), validate(length(min = 1)))]pub body: String,#[expose] carries the validate(...) attribute through to
CreatePostDto and UpdatePostDto. No second declaration on the
DTO; the entity is the single source.
The bad-body response
Section titled “The bad-body response”A malformed body comes back as a JSON 400 with the structured field
errors validator produces:
$ curl -i -X POST http://localhost:3005/posts \ -H 'Content-Type: application/json' \ -d '{"title":"","body":"World"}'HTTP/1.1 400 Bad Requestcontent-type: application/json
{ "statusCode": 400, "error": "Bad Request", "message": "validation failed", "details": { "title": [{ "code": "length", "params": { "min": 1, "value": "" } }] }}The failing field surfaces in details. The client can render per-field
messages from details.<field>[].code without parsing the human message.
The good-body path is unchanged
Section titled “The good-body path is unchanged”A well-formed body still reaches the handler exactly as before — the pipe is a no-op on successful validation.
$ curl -X POST http://localhost:3005/posts \ -H 'Content-Type: application/json' \ -d '{"title":"Hello","body":"World"}'HTTP/1.1 500 Internal Server ErrorStill 500 — the DB isn’t wired yet — but the body cleared validation
and the handler ran. Page 6 closes that gap.
Two failure modes worth the distinction
Section titled “Two failure modes worth the distinction”| Failure | Status | Trigger |
|---|---|---|
| The body isn’t valid JSON | 400 | poem rejects before any pipe runs |
| The body parses but a field is wrong | 400 with details | ValidationPipe rejects after extraction |
Both are 400; the second carries the structured details a client
form binds to. The handler never sees either.
A custom rule with #[validate(custom)]
Section titled “A custom rule with #[validate(custom)]”When validator’s built-in rules aren’t enough, point at a function:
#[derive(Deserialize, Validate)]pub struct CreatePostDto { #[validate(length(min = 1), custom(function = "not_a_placeholder_title"))] pub title: String, pub body: String,}
fn not_a_placeholder_title(title: &str) -> Result<(), validator::ValidationError> { if title.eq_ignore_ascii_case("todo") { return Err(validator::ValidationError::new("placeholder_title")); } Ok(())}The error code ("placeholder_title") surfaces in details.title[].code
— same shape, same client contract.
What you have now
Section titled “What you have now”- A
POST /postsroute that rejects malformed bodies before they reach the service — wired by#[crud], not by hand. - A structured
400body with one entry per failing field, codes stable enough to assert on. - A pattern you carry forward: entity rules → generated or overridden
handlers that take
Valid<Json<E>>.