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 };