baguilar6174 / node-template-server

Boilerplate Node projects with Express, Typescript and Clean Architecture
https://baguilar6174.medium.com/modern-api-development-with-node-js-express-and-typescript-using-clean-architecture-0868607b76de
79 stars 21 forks source link

separate validation from dto definition and using zod library for validation #1

Open bensaadmohamed opened 3 months ago

bensaadmohamed commented 3 months ago

hello and thanks for this clean code but i see that you re implementing the dto validation inside the same class my suggestion to use an external library like zod to do the validation in separate class or layer for example: 1.create a schema directory inside domain directory 2.create a file auth.schema.ts inside it


export const registerUserSchema = object({
  body: object({
    name: string({
      required_error: 'Name is required',
    }),
    email: string({
      required_error: 'Email address is required',
    }).email('Invalid email address'),
    password: string({
      required_error: 'Password is required',
    })
      .min(8, 'Password must be more than 8 characters')
      .max(32, 'Password must be less than 32 characters'),
    passwordConfirm: string({
      required_error: 'Please confirm your password',
    }),

  }).refine((data) => data.password === data.passwordConfirm, {
    path: ['passwordConfirm'],
    message: 'Passwords do not match',
  }),
});

export const loginUserSchema = object({
  body: object({
    email: string({
      required_error: 'Email address is required',
    }).email('Invalid email address'),
    password: string({
      required_error: 'Password is required',
    }).min(8, 'Invalid email or password'),
  }),
});

export const verifyEmailSchema = object({
  params: object({
    verificationCode: string(),
  }),
});

export type RegisterUserInput = Omit<TypeOf<typeof registerUserSchema>['body'],'passwordConfirm'>;
export type LoginUserInput = TypeOf<typeof loginUserSchema>['body'];
export type VerifyEmailInput = TypeOf<typeof `verifyEmailSchema>['params'];

3.create a validation middleware in shared module

import { type Response, type NextFunction, type Request } from 'express';
import { AnyZodObject, ZodError } from 'zod';

export class ValidateMiddlewares {
    //* Dependency injection
    // constructor() {}

    public static validate = (schema: AnyZodObject) => (req: Request, res: Response, next: NextFunction) => {

        try {
            schema.parse({
                params: req.params,
                query: req.query,
                body: req.body,
            });

            next();
        } catch (error) {
            if (error instanceof ZodError) {
                return res.status(400).json({
                    status: 'fail',
                    errors: error.errors,
                });
            }
            next(error);
        }
    };
}

4.the login.dto.ts and register.dto.ts becomes

import { RegisterUserInput } from "../schemas";

/**
 * DTOs must have a validate method that throws an error
 * if the data is invalid or missing required fields.
 */
export class RegisterUserDto  {
    private constructor(
        public readonly name: string,
        public readonly email: string,
        public readonly password: string
    ) {

    }

    public static create(input: RegisterUserInput): RegisterUserDto {
        const { name, email, password } = input;
        return new RegisterUserDto(name as string, email as string, password as string);
    }
}
import { LoginUserInput } from '../schemas';

/**
 * DTOs must have a validate method that throws an error
 * if the data is invalid or missing required fields.
 */
export class LoginUserDto  {
    private constructor(
        public readonly email: string,
        public readonly password: string
    ) {

    }

    public static create(input: LoginUserInput): LoginUserDto {
        const { email, password } = input;
        return new LoginUserDto(email as string, password as string);
    }
}

5.auth controller.ts

import { HttpCode, type SuccessResponse } from '../../../core';
import {
    type AuthRepository,
    RegisterUserDto,
    LoginUser,
    type AuthEntity,
    RegisterUser,
    LoginUserDto
} from '../domain';
import { LoginUserInput, RegisterUserInput } from '../domain/schemas';

export class AuthController {
    //* Dependency injection
    constructor(private readonly repository: AuthRepository) { }

    public login = (
        req: Request<unknown, unknown, LoginUserInput>,
        res: Response<SuccessResponse<AuthEntity>>,
        next: NextFunction
    ): void => {

        const dto = LoginUserDto.create(req.body);
        new LoginUser(this.repository)
            .execute(dto)
            .then((result) => res.json({ data: result }))
            .catch(next);
    };

    public register = (
        req: Request<unknown, unknown, RegisterUserInput>,
        res: Response<SuccessResponse<AuthEntity>>,
        next: NextFunction
    ): void => {
        const dto = RegisterUserDto.create(req.body);
        new RegisterUser(this.repository)
            .execute(dto)
            .then((result) => res.status(HttpCode.CREATED).json({ data: result }))
            .catch(next);
    };
}

6.auth routes

import { Router } from 'express';

import { AuthController } from './controller';
import { AuthDatasourceImpl, AuthRepositoryImpl } from '../infrastructure';
import { ValidateMiddlewares } from '../../shared';
import { loginUserSchema, registerUserSchema } from '../domain/schemas';

export class AuthRoutes {
    static get routes(): Router {
        const router = Router();

        const datasource = new AuthDatasourceImpl();
        const repository = new AuthRepositoryImpl(datasource);
        const controller = new AuthController(repository);

        router.post('/login',ValidateMiddlewares.validate(loginUserSchema) ,controller.login);
        router.post('/register',ValidateMiddlewares.validate(registerUserSchema), controller.register);

        return router;
    }
}
baguilar6174 commented 17 hours ago

Thank you very much for the contribution, I will be implementing it in a new branch, thanks for the support 💪