2. Make it trustworthy
We left chapter 1 with a working slice and an honest problem: a librarian can register an author with a blank name, or the same author twice, and nothing stops them. A trustworthy app has to be able to say no — clearly, and as early as possible.
There are two different kinds of “no” here, and Arc handles them in two different places:
- “That input is malformed.” A blank name is wrong on its face — you don’t need to look at anything else to know it. That’s a validation rule, and it belongs on the value.
- “That input collides with what already exists.” A duplicate name is only wrong relative to the rest of the system. That’s a business rule, and it needs to consult state.
Let’s add both.
Validate the value, once
Section titled “Validate the value, once”Your first instinct might be to validate Name inside a RegisterAuthor validator. But think about it: every command that takes an AuthorName wants the same guarantee — never blank. Arc lets you state that rule on the value type itself, so it’s enforced everywhere an AuthorName is used, and you never write it twice.
-
Write a
ConceptValidatorfor the value type. It reaches into the concept’s underlyingValueand applies ordinary FluentValidation rules:public class AuthorNameValidator : ConceptValidator<AuthorName>{public AuthorNameValidator(){RuleFor(x => x.Value).NotEmpty().WithMessage("An author needs a name.");}}That’s the whole change. You don’t register it anywhere — Arc discovers validators by convention. From now on, any command carrying an
AuthorNamerejects a blank one beforeHandle()ever runs.
Reject the duplicate against existing state
Section titled “Reject the duplicate against existing state”Uniqueness is the harder kind of “no”, because it depends on what’s already there. Input validation can’t answer it — you have to look in the database. That’s a job for a CommandValidator, which validates a whole command and, because Arc constructs it from the container, can take the dependencies it needs to check state.
-
Write a
CommandValidatorthat asks the database. AMustAsyncrule looks for an author already carrying this name; if it finds one, the command is rejected:public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>{public RegisterAuthorValidator(IMongoCollection<Author> authors){RuleFor(c => c.Name).MustAsync(async (name, _) => !await authors.Find(a => a.Name == name).AnyAsync()).WithMessage("An author with that name is already registered.");}}public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>{public RegisterAuthorValidator(LibraryDbContext db){RuleFor(c => c.Name).MustAsync(async (name, ct) => !await db.Authors.AnyAsync(a => a.Name == name, ct)).WithMessage("An author with that name is already registered.");}}Handle()doesn’t change — it stays the clean write from chapter 1. The validator runs first; if it fails,Handle()never executes.
The UI already knows
Section titled “The UI already knows”Here’s the part that feels like a gift. You didn’t touch the React code from chapter 1 — but the form now validates.
When you wrote the ConceptValidator, the proxy generator extracted that rule into the generated TypeScript too, so the CommandDialog runs it on the client and disables the confirm button on a blank name — no round trip. The uniqueness rule can’t run on the client (it has to look in the database), so it runs on the server; when it rejects, the message comes back through the same proxy and CommandDialog renders it against the form — “An author with that name is already registered.” — because the dialog wires command results to fields for you.
<CommandDialog<RegisterAuthor> command={RegisterAuthor} title="Add author" okLabel="Add"> <InputTextField<RegisterAuthor> value={i => i.name} title="Name" /></CommandDialog>One rule on the value type, one rule on the command, and both ends of the app honor them — because the validation, like the types, flows from one source.
What you built
Section titled “What you built”- An
AuthorNameValidatorthat guards the value type, enforced everywhere anAuthorNameis used and run client-side through the generated proxy. - A
RegisterAuthorValidatorbusiness rule that checks existing state in the database, with a unique index behind it for the hard guarantee. - A React form that surfaces both — client-side and server-side — with no extra frontend code.
Our author is trustworthy now. But an author with no books is a lonely thing. Next we’ll give them a catalog — a second feature that relates to this one. Let’s add books →