1

TL;DR What are common practices to separate validation logic inside a rest API & thereby keep the code clean and straight forward? (code example)

Context & Example

Let's say I'm developing a rest API to handle some basic authentication. This example endpoint is responsible for confirming the user's e-mail after sign up.

There needs to be done a whole bunch of validation before the e-mail is allowed to be confirmed. This validation can be found inside the validateBeforeSave() function.

Question

Should the validation logic be separated? I find this file rather messy, you lose track of what exactly is happening in this route.

I'm not sure how to clean this up, what common practices are to do so and/or what architecture you can use to avoid this mess.

Or is this absolutely fine/acceptable?

My Steps

Things I've done to already reduce validation inside the rest API is using a class-validators in the form of a Data Transfer Object (Dto). This is used to validate the incoming request data, used as validateRequestMiddleware(ConfirmDto, true)

const router = express.Router();
    
router.get('/api/user/confirm/:token',
    validateRequestMiddleware(ConfirmDto, true),
    currentUserMiddleware,
    requireAuthMiddleware(false),
    async (req: Request, res: Response) => {
        const { token } = req.params;
        const user: UserAuthTokenDto = req.currentUser!;
    
        const { confirmation, repository } = await validateBeforeSave(user, token);
    
        confirmation.status = EmailConfirmationStatus.Confirmed;
        await repository.save(confirmation);
    
        await new UserEmailConfirmedPublisher().publish({
            user: {
                id: confirmation.userId
            }
        });
    
        await updateAuthToken(req, {id: user.userId, email: user.email});
    
        res.status(200).send(confirmation);
    }
);
    
const validateBeforeSave = async (user: UserAuthTokenDto, token: string):
    Promise<{
        confirmation: EmailConfirmation,
        repository: Repository<EmailConfirmation>
    }> => {
    
    const confirmationRepository = getRepository(EmailConfirmation);
    const emailConfirmation = await confirmationRepository.findOne({ where: { token: token } });
    
    // Validate if email confirmation exists
    if (!emailConfirmation)
        throw new BadRequestError('No such confirmation token found');
    
    // Validate if email confirmation belongs to the user (request sender)
    if (emailConfirmation.userId !== user.userId)
        throw new NotAuthorizedError();
    
    const allEmailConfirmationsFromUser = await confirmationRepository.find({ where: { userId: emailConfirmation.userId } });
    
    // Validate if email already has been confirmed
    for (let ev of allEmailConfirmationsFromUser)
        if (ev.status === EmailConfirmationStatus.Confirmed)
            throw new BadRequestError('This account is already confirmed');
    
    // Validate if email is still awaiting validation
    if (emailConfirmation.status !== EmailConfirmationStatus.Awaiting)
        throw new BadRequestError('This confirmation token is not awaiting validation anymore');
    
    return {
        confirmation: emailConfirmation,
        repository: confirmationRepository
    }
}
    
export { router as confirmEmailRouter };
2
  • 1
    You should add a language tag. It will help the site display your code. Alternately you can mark the language in your code section.
    – JimmyJames
    Jan 27, 2021 at 21:51
  • Thanks I changed it to javascript since the typescript identifier isn't supported (currently).
    – Michiel
    Jan 31, 2021 at 13:45

2 Answers 2

1

In this situation, I would generally define a ValidationRule class. The (very) basic idea is that you then create collections (lists, dictionaries, etc.) of rules and loop over them, passing your data to each validation object. If any one rule returns 'NO', the entire validation fails. I would also give each of these objects a message() method to explain what went wrong. (note: you may not always want to expose this information to the client. It depends on the use case)

Once you have this basic structure of this down, you may want to create some method of loading these rules from something other than code. For example you might want to define it as a set of YAML or JSON structures or store these rules in a DB.

This approach can be as simple or sophisticated as you need. For example, you might want to have rule that is composed of other rules. You might want to run through all the rules even if you've already determined the validation is failed.

The basic idea is to compartmentalize the logic. Your main flow has a validation step that delegates to this subsystem. The validation subsystem is entirely focused on validation and nothing else. This not only allows you to deal with your own personal mental clutter but potentially allows the validation rules to be delegated to another person entirely. That person might not even be a developer.

1

I assume that the validation logic is the same when the user signs up and if the user wants to change email at a later time. If that is the case you don't want to duplicate the validation logic because it will be prone to drift in different directions. That speaks for separating the validation logic into a separate class to allow reuse.

Since the repository is used both to fetch the confirmation and saving the status, I would probably put the validation logic into the repository.

It will have the effect that it can't be bypassed or forgotten when changing an email.

Depending on the complexity of the validation you could put the code directly in the repository or inject a validation class. I would probably go with injecting a class because the validation is a bit complex.

You could look into validation and aggregate roots in the DDD methodology there are some great insights on how to structure validation. There are several approaches.

A link to get you started

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.