Skip to content

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.

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.

  1. Write a ConceptValidator for the value type. It reaches into the concept’s underlying Value and 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 AuthorName rejects a blank one before Handle() 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.

  1. Write a CommandValidator that asks the database. A MustAsync rule 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.");
    }
    }

    Handle() doesn’t change — it stays the clean write from chapter 1. The validator runs first; if it fails, Handle() never executes.

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.

AddAuthor.tsx — unchanged from chapter 1
<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.

  • An AuthorNameValidator that guards the value type, enforced everywhere an AuthorName is used and run client-side through the generated proxy.
  • A RegisterAuthorValidator business 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 →