0

Suppose I have a web application that processes a request as follows.

  1. A user fills in a form and submits it. (e.g., to register as a new user for my application)
  2. The request is passed to the appropriate view/controller in the backend of my application. (e.g., a request CREATE /user is passed on to a UserController)
  3. This controller passes on the request to a repository in the data layer (e.g., to a UserRepository, which is responsible for storing new users by communicating with a database)
  4. This repository accesses the database and performs the necessary queries (e.g., add a new row to a user table containing the user's data)

At which point(s) should I validate the request?

To take the simple example of registering a new user, I should make sure that its mail address is in the correct format. I know that I should definitely validate this in the frontend for fast feedback, and that I should definitely validate this in the backend too because the frontend cannot be trusted. However, at which point in the backend should this be done?

Some thoughts:

  • Validating the mail address inside the UserRepository will make sure that other application logic will not accidentally insert invalid users into the database.
  • I think implementing validation rules such as validating mail addresses inside the database itself might be infeasible (both practically and efficiency-wise)
  • I typically see that validation is only done in views/controllers to validate incoming requests. For example, I'm currently using the Nest.js framework, which uses a concept called validation pipes to validate data right before they reach the implementation of a controller. They don't mention anything about validation after that.
  • Validating both inside UserController and UserRepository will check the same things twice, which seems unnecessary.
1
  • stage 1. other stages just throw exceptions
    – Ewan
    May 13, 2022 at 19:48

1 Answer 1

4

Typically, as early as possible – validation should happen at a system boundary, but you'll have to decide where exactly to draw that boundary.

When working in a typed language, a good rule of thumb is that invalid states should be unrepresentable. That means, at some point you likely translate the completely untrusted data from the request into some value type that you guarantee will have a particular shape. At that point, you can perform validation. You will find that such validation will typically happen fairly early, usually in a Controller or Use Case.

For example, you might want to validate an email address. If you pass the email address around as a String, it is not clear where validation should occur. On the other hand, if you create an EmailAddress type, then there is a clear conversion at some point. The Repository shouldn't do this conversion – it makes sense if it operates on an EmailAddress. Similarly, other user registration business logic should likely be able trust that the EmailAddress is syntactically correct (though other validation steps like sending a verification email should be part of the business logic for that use case). The result is that typically at a very early point will the transfer representation be translated into an internal data model, and at that point basic validation can be applied.

For example, we might define the following data model and conversion functions:

class RegisterUserRequest {
  readonly name: Username;
  readonly password: Password;
  readonly email: EmailAddress;
}

class Username { readonly value: string }
class Password { readonly value: string }
class EmailAddress { readonly value: string }

type Result<T> = T | { error: string }

const parseRegisterUserRequest = (data: unknown): Result<RegisterUserRequest> => ...;

const parseUsername (data: unknown): Result<Username> => ...;

const parsePassword (data: unknown): Result<Password> => {
  if (!(data instanceof String)) return { error: "password must be string" };
  if (data.length < 8) return { error: "password must be 8 characters or longer" };
  return new Password(data);
};

const parseEmailAddress (data: unknown): Result<EmailAddress> => ...;

Now functions deeper within our application can trust that a RegisterUserRequest and an EmailAddress are already syntactically correct. For example:

const registerUser = ({name, password, email}: RegisterUserRequest): User => ...;

And this means that a controller will have to perform the necessary translation. For example:

app.post('/register', req => {
  const data = parseRegisterUserRequest(await req.json());
  if (data.error) return respond(400, data.error);

  const user = usecase.registerUser(data);

  return respond(200, `hello, ${user.name}!`);
})

(Please note that this answer uses TypeScript syntax merely for illustrative purposes. This code would not actually be type-safe since TypeScript considers instances of different classes of the same shape to be compatible, making approaches like brands necessary instead.)

The validation pipelines you mentioned from NestJS take exactly this idea of defining a type that must have a particular shape, and validating the input data before an instance of this type is created. However, there the validation is defined using decorators in a more declarative fashion rather than needing more cumbersome ordinary code as suggested in this answer. The equivalent would be:

class RegisterUserRequest {
  @...
  name: string;

  @MinLength(8, { message: "password must be 8 characters or longer" })
  @IsString()
  password: string;

  @...
  email: string;
}
3
  • I like the idea of strongly typed validation very much! This guarantees a minimal amount of validation without losing safety. Some questions/remarks: (1) You justly mention the use of brands to simulate nominal typing, as TypeScript uses structural typing. There are alternatives that don't have the (admittedly small) overhead of classes: see this post and here. (2) Is there a reason for returning an {error: string} object instead of throwing an exception? ...
    – Safron
    May 15, 2022 at 10:04
  • 1
    (3) A practical question: you give an example of how this would work in NestJS with decorators. However, it seems that this approach doesn't have the benefits of strong typing, so I guess application logic in deeper layers might still accidentally perform unsafe operations. Do you know a way of combining both approaches?
    – Safron
    May 15, 2022 at 10:06
  • 1
    @Safron (1) I don't see the class-based alternative, all of them use some variant of the brands pattern. (2) You can definitely use exceptions, but using error values might make it clearer that this operation is fallible and might make it easier to collect multiple validation errors. (3) You will not be able to create an object that is instanceof RegisterUserRequest and violates the validation constraints. This might be good enough. I am not aware of easy ways to combine NestJS validation with nominal typing. I'd expect that you'd need two separate types if you want to go this route.
    – amon
    May 15, 2022 at 11:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.