DEV Community

Yukti Sahu
Yukti Sahu

Posted on

Validations and Transformations in Backend Development

Backend Architecture — The Layer Cake of Backend Development

Think of your backend as a well-organized kitchen where each station has a specific job. This document breaks down the major backend layers and examples (JavaScript / Node/Express code) for each.

┌─────────────────────────────────────┐
│     Middleware (Validation)         │  ← Guards the entrance
├─────────────────────────────────────┤
│     Controllers (HTTP Handlers)     │  ← Manages requests/responses
├─────────────────────────────────────┤
│     Service Layer (Business Logic)  │  ← Where the magic happens
├─────────────────────────────────────┤
│     Repository (Database Access)    │  ← Talks to the database
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

1) Repository Layer (Bottom Layer)

This is the foundation — it directly talks to your database.

Why separate this layer?

  • Keeps SQL or DB queries in one place
  • Easier to switch databases (MySQL → PostgreSQL) by only changing repository code
  • Easier to unit test database operations independently

Example: User Repository

// Example: UserRepository
class UserRepository {
  // This method ONLY deals with database operations
  async findUserByEmail(email) {
    // Using a database query to fetch user
    return await db.query('SELECT * FROM users WHERE email = ?', [email]);
  }

  async createUser(userData) {
    // Insert new user into database
    return await db.query(
      'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
      [userData.name, userData.email, userData.password]
    );
  }

  async updateUserLastLogin(userId) {
    // Update user's last login timestamp
    return await db.query(
      'UPDATE users SET last_login = NOW() WHERE id = ?',
      [userId]
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

2) Service Layer (Business Logic Layer)

This is where your application's brain lives. It orchestrates multiple operations and calls repository functions, external services, etc.

Why this layer is important:

  • Reusability: calling the same business logic from different controllers or handlers
  • Easier to unit test business rules without HTTP semantics
  • Keeps controller thin

Example: User Service

class UserService {
  constructor(userRepository, emailService, webhookService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
    this.webhookService = webhookService;
  }

  async registerUser(userData) {
    const existingUser = await this.userRepository.findUserByEmail(userData.email);
    if (existingUser) {
      throw new Error('User already exists with this email');
    }

    const hashedPassword = await bcrypt.hash(userData.password, 10);

    const newUser = await this.userRepository.createUser({
      ...userData,
      password: hashedPassword
    });

    await this.emailService.sendWelcomeEmail(newUser.email, newUser.name);

    await this.webhookService.notifyUserCreated(newUser.id);

    return {
      id: newUser.id,
      name: newUser.name,
      email: newUser.email
    };
  }

  async loginUser(email, password) {
    const user = await this.userRepository.findUserByEmail(email);
    if (!user) {
      throw new Error('Invalid credentials');
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      throw new Error('Invalid credentials');
    }

    await this.userRepository.updateUserLastLogin(user.id);

    const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);

    return { token, user: { id: user.id, name: user.name, email: user.email } };
  }
}
Enter fullscreen mode Exit fullscreen mode

3) Controller Layer (HTTP Handler)

This layer handles Express-specific logic: reading req, sending res, HTTP status codes, and calling services.

Controller Example: User Controller

class UserController {
  constructor(userService) {
    this.userService = userService;
  }

  // Handle user registration endpoint
  async register(req, res) {
    try {
      const { name, email, password } = req.body;
      const result = await this.userService.registerUser({ name, email, password });

      return res.status(201).json({
        success: true,
        message: 'User registered successfully',
        data: result
      });

    } catch (error) {
      return res.status(400).json({
        success: false,
        message: error.message
      });
    }
  }

  // Handle user login endpoint
  async login(req, res) {
    try {
      const { email, password } = req.body;
      const result = await this.userService.loginUser(email, password);

      return res.status(200).json({
        success: true,
        message: 'Login successful',
        data: result
      });

    } catch (error) {
      return res.status(401).json({
        success: false,
        message: error.message
      });
    }
  }

  // Handle getting user profile
  async getProfile(req, res) {
    try {
      const userId = req.user.id;
      const user = await this.userService.getUserById(userId);

      return res.status(200).json({ success: true, data: user });
    } catch (error) {
      return res.status(404).json({ success: false, message: 'User not found' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4) Middleware Layer (The Gatekeeper)

Middleware sits before controllers to enforce security, validation, and transformation.

Example: Authentication Middleware

const authMiddleware = (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
      return res.status(401).json({ success: false, message: 'No token provided, authorization denied' });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ success: false, message: 'Invalid token' });
  }
};
Enter fullscreen mode Exit fullscreen mode

Example: Validation Middleware

const validateUserRegistration = (req, res, next) => {
  const { name, email, password } = req.body;

  if (!name || !email || !password) {
    return res.status(400).json({ success: false, message: 'Name, email, and password are required' });
  }

  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    return res.status(400).json({ success: false, message: 'Invalid email format' });
  }

  if (password.length < 8) {
    return res.status(400).json({ success: false, message: 'Password must be at least 8 characters long' });
  }

  next();
};

// Using middleware in routes
app.post('/register', validateUserRegistration, userController.register);
app.get('/profile', authMiddleware, userController.getProfile);
Enter fullscreen mode Exit fullscreen mode

Why use middleware?

  • Security: stop unauthorized access BEFORE business logic
  • Data validation: catch bad data early
  • Reuse logic across routes
  • Keep controllers focused on business flow

Understanding Errors: The Three Types

Errors encountered while validating/processing data typically fall into these categories: Syntactic, Type validation, and Semantic validation.

1) Syntactic Errors (Structure Problems)

These are errors where the data format itself is wrong.

const validateJsonSyntax = (req, res, next) => {
  if (!req.is('application/json')) {
    return res.status(400).json({ error: 'Syntactic Error', message: 'Content-Type must be application/json' });
  }
  next();
};
Enter fullscreen mode Exit fullscreen mode

Common syntactic issues:

  • Missing quotes: {name: John} (invalid) vs {"name":"John"} (valid)
  • Trailing commas
  • Using single quotes for JSON

2) Type Validation Errors (Wrong Data Type)

The structure is correct, but data types are incorrect.

const validateUserTypes = (req, res, next) => {
  const { name, age, email, isActive } = req.body;

  if (typeof name !== 'string') return res.status(400).json({ error: 'Type Validation Error', message: 'Name must be a string' });
  if (typeof age !== 'number') return res.status(400).json({ error: 'Type Validation Error', message: 'Age must be a number' });
  if (typeof email !== 'string') return res.status(400).json({ error: 'Type Validation Error', message: 'Email must be a string' });
  if (typeof isActive !== 'boolean') return res.status(400).json({ error: 'Type Validation Error', message: 'isActive must be a boolean' });

  next();
};
Enter fullscreen mode Exit fullscreen mode

3) Semantic Validation Errors (Wrong Meaning/Logic)

Type is correct, but the value doesn't make sense in your business context.

const validateUserSemantics = (req, res, next) => {
  const { age, email, birthDate, country } = req.body;

  if (age < 0 || age > 150) {
    return res.status(400).json({ error: 'Semantic Validation Error', message: 'Age must be between 0 and 150' });
  }

  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    return res.status(400).json({ error: 'Semantic Validation Error', message: 'Email format is invalid' });
  }

  const birth = new Date(birthDate);
  if (birth > new Date()) {
    return res.status(400).json({ error: 'Semantic Validation Error', message: 'Birth date cannot be in the future' });
  }

  const validCountries = ['US', 'UK', 'IN', 'CA', 'AU'];
  if (!validCountries.includes(country)) {
    return res.status(400).json({ error: 'Semantic Validation Error', message: 'Invalid country code', allowedValues: validCountries });
  }

  next();
};
Enter fullscreen mode Exit fullscreen mode

Transformations: Shaping Data Into What You Need

Transformations convert incoming data into a consistent format suitable for your business logic and storage.

Why transform?

  • Normalize casing
  • Trim whitespace
  • Convert strings to numbers, booleans, dates
  • Remove special chars (e.g., phone formatting)

Frontend client example

Input:

{
  "name": "  John Doe  ",
  "email": "JOHN@EXAMPLE.COM",
  "phone": "123-456-7890",
  "age": "25"
}
Enter fullscreen mode Exit fullscreen mode

After server transform:

{
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "1234567890",
  "age": 25
}
Enter fullscreen mode Exit fullscreen mode

Example: Transformation Middleware

const transformUserInput = (req, res, next) => {
  const { name, email, phone, age } = req.body;
  const transformedData = {};

  if (name) transformedData.name = name.trim().split(' ').map(w => w.charAt(0).toUpperCase()+w.slice(1).toLowerCase()).join(' ');
  if (email) transformedData.email = email.trim().toLowerCase();
  if (phone) transformedData.phone = phone.replace(/\D/g, '');
  if (age) transformedData.age = parseInt(age, 10);

  req.body = transformedData;
  next();
};

// Usage
app.post('/users', transformUserInput, validateUserTypes, userController.register);
Enter fullscreen mode Exit fullscreen mode

Query Parameter Transformation

const transformQueryParams = (req, res, next) => {
  const { page, limit, sortBy, minPrice, inStock } = req.query;
  req.queryParams = {
    page: page ? parseInt(page, 10) : 1,
    limit: limit ? parseInt(limit, 10) : 20,
    sortBy: sortBy || 'createdAt',
    minPrice: minPrice ? parseFloat(minPrice) : 0,
    inStock: inStock === 'true'
  };

  if (req.queryParams.page < 1) return res.status(400).json({ error: 'Invalid page number' });
  if (req.queryParams.limit < 1 || req.queryParams.limit > 100) return res.status(400).json({ error: 'Invalid limit' });

  next();
};
Enter fullscreen mode Exit fullscreen mode

Date Transformation & Validation

const transformDates = (req, res, next) => {
  const { startDate, endDate, birthDate } = req.body;

  if (startDate) {
    req.body.startDate = new Date(startDate);
    if (isNaN(req.body.startDate.getTime())) return res.status(400).json({ error: 'Invalid startDate format' });
  }

  if (endDate) {
    req.body.endDate = new Date(endDate);
    if (isNaN(req.body.endDate.getTime())) return res.status(400).json({ error: 'Invalid endDate format' });
  }

  if (birthDate) {
    const birth = new Date(birthDate);
    if (isNaN(birth.getTime())) return res.status(400).json({ error: 'Invalid birthDate format' });

    const today = new Date();
    let age = today.getFullYear() - birth.getFullYear();
    const monthDiff = today.getMonth() - birth.getMonth();
    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) age--;

    req.body.birthDate = birth;
    req.body.age = age;
  }

  next();
};
Enter fullscreen mode Exit fullscreen mode

Client-Side vs Server-Side Validation

Think of client-side validation as a helpful sign (UX) and server-side validation as security screening — both are necessary.

Client-Side (React example)

function RegistrationForm() {
  const [errors, setErrors] = useState({});

  const validateForm = (formData) => {
    const newErrors = {};
    if (!formData.name) newErrors.name = 'Name is required';
    if (!formData.email) newErrors.email = 'Email is required';
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) newErrors.email = 'Invalid format';
    if (!formData.password) newErrors.password = 'Password is required';
    else if (formData.password.length < 8) newErrors.password = 'Password must be at least 8 characters';

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  // ...handle submit, fetch API
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Better UX, instant feedback
  • Reduces server load
  • Faster response

Limitations:

  • Not secure (easily bypassed)
  • Can't verify database constraints

Server-Side (Node/Express example)

const validateRegistration = async (req, res, next) => {
  const { name, email, password } = req.body;
  const errors = {};

  if (!name || !name.trim()) errors.name = 'Name is required and cannot be empty';
  if (!email) errors.email = 'Email is required';
  else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = 'Invalid email format';
  else {
    const existingUser = await userRepository.findByEmail(email);
    if (existingUser) errors.email = 'Email is already registered';
  }

  if (!password) errors.password = 'Password is required';
  else if (password.length < 8) errors.password = 'Password must be at least 8 characters';

  if (Object.keys(errors).length > 0) return res.status(400).json({ success: false, errors });

  next();
};
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Security and data integrity
  • Database checks and complex business rules

The Golden Rule: Always Do Both — client for UX, server for security.


How Developers Should Approach This (DRY principle)

  • Define validation rules in a single place and reuse them across frontend and backend
  • Create a generic validator function and re-use it across layers

Example: Single Source Validation Rules

const validationRules = {
  name: { required: true, minLength: 2, maxLength: 50, pattern: /^[a-zA-Z\s]+$/ },
  email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
  password: { required: true, minLength: 8, maxLength: 100, pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/ }
};

function validateField(value, rules) {
  if (rules.required && !value) return 'Field is required';
  if (rules.minLength && value.length < rules.minLength) return `Must be at least ${rules.minLength} characters`;
  if (rules.maxLength && value.length > rules.maxLength) return `Must be at most ${rules.maxLength} characters`;
  if (rules.pattern && !rules.pattern.test(value)) return 'Invalid format';
  return null;
}

// Reuse the same functions on both frontend and backend
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Complete User Registration Flow

A complete example: shared validation rules, transform middleware, validation middleware, repository, service, controller, and router.

1) Validation Rules (shared)

const userValidationRules = {
  name: { required: true, minLength: 2, maxLength: 50, errorMessages: { required: 'Name is required', minLength: 'Name must be at least 2 characters', maxLength: 'Name cannot exceed 50 characters' } },
  email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, errorMessages: { required: 'Email is required', pattern: 'Please enter a valid email address' } },
  password: { required: true, minLength: 8, pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/, errorMessages: { required: 'Password is required', minLength: 'Password must be at least 8 characters', pattern: 'Password must contain uppercase, lowercase, number, and special character' } },
  age: { required: true, min: 18, max: 120, errorMessages: { required: 'Age is required', min: 'You must be at least 18 years old', max: 'Please enter a valid age' } }
};
Enter fullscreen mode Exit fullscreen mode

2) Transformation Middleware

const transformUserInput = (req, res, next) => {
  try {
    const { name, email, password, age } = req.body;
    req.body = {
      name: name ? name.trim() : '',
      email: email ? email.trim().toLowerCase() : '',
      password: password || '',
      age: age ? parseInt(age, 10) : null
    };
    next();
  } catch (error) {
    return res.status(400).json({ success: false, message: 'Error processing input data', error: error.message });
  }
};
Enter fullscreen mode Exit fullscreen mode

3) Validation Middleware

const validateUserRegistration = async (req, res, next) => {
  const { name, email, password, age } = req.body;
  const errors = {};

  // Name
  if (!name) errors.name = userValidationRules.name.errorMessages.required;
  else if (name.length < userValidationRules.name.minLength) errors.name = userValidationRules.name.errorMessages.minLength;
  else if (name.length > userValidationRules.name.maxLength) errors.name = userValidationRules.name.errorMessages.maxLength;

  // Email
  if (!email) errors.email = userValidationRules.email.errorMessages.required;
  else if (!userValidationRules.email.pattern.test(email)) errors.email = userValidationRules.email.errorMessages.pattern;
  else {
    const existingUser = await userRepository.findByEmail(email);
    if (existingUser) errors.email = 'This email is already registered';
  }

  // Password
  if (!password) errors.password = userValidationRules.password.errorMessages.required;
  else if (password.length < userValidationRules.password.minLength) errors.password = userValidationRules.password.errorMessages.minLength;
  else if (!userValidationRules.password.pattern.test(password)) errors.password = userValidationRules.password.errorMessages.pattern;

  // Age
  if (!age) errors.age = userValidationRules.age.errorMessages.required;
  else if (age < userValidationRules.age.min) errors.age = userValidationRules.age.errorMessages.min;
  else if (age > userValidationRules.age.max) errors.age = userValidationRules.age.errorMessages.max;

  if (Object.keys(errors).length > 0) return res.status(400).json({ success: false, message: 'Validation failed', errors });

  next();
};
Enter fullscreen mode Exit fullscreen mode

4) Repository Layer (simplified)

class UserRepository {
  async findByEmail(email) {
    return await db.query('SELECT * FROM users WHERE email = ?', [email]);
  }
  async create(userData) {
    const result = await db.query('INSERT INTO users (name, email, password, age, created_at) VALUES (?, ?, ?, ?, NOW())', [userData.name, userData.email, userData.password, userData.age]);
    return { id: result.insertId, ...userData };
  }
}
Enter fullscreen mode Exit fullscreen mode

5) Service Layer

class UserService {
  constructor(userRepository, emailService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
  }

  async registerUser(userData) {
    const hashedPassword = await bcrypt.hash(userData.password, 10);
    const newUser = await this.userRepository.create({ ...userData, password: hashedPassword });
    // Send welcome email (async)
    this.emailService.sendWelcomeEmail(newUser.email, newUser.name).catch(err => console.error('Email error:', err));
    return { id: newUser.id, name: newUser.name, email: newUser.email, age: newUser.age };
  }
}
Enter fullscreen mode Exit fullscreen mode

6) Controller Layer

class UserController {
  constructor(userService) {
    this.userService = userService;
  }
  async register(req, res) {
    try {
      const result = await this.userService.registerUser(req.body);
      return res.status(201).json({ success: true, message: 'User registered successfully', data: result });
    } catch (error) {
      console.error('Registration error:', error);
      return res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? error.message : undefined });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

7) Routes (Putting it together)

const express = require('express');
const router = express.Router();

const userRepository = new UserRepository();
const emailService = new EmailService();
const userService = new UserService(userRepository, emailService);
const userController = new UserController(userService);

router.post('/register', transformUserInput, validateUserRegistration, userController.register.bind(userController));

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Key Takeaways ✅

  • Layer Separation is critical: each layer has one job (Repository, Service, Controller, Middleware)
  • Validation prevents disasters: syntactic, type-based, and semantic checks are all necessary
  • Transform data early: normalize types and formats before they reach your business logic
  • Client + Server validation = complete protection (UX + Security)
  • Middleware is your friend: reusable, consistent, and keeps controllers focused

By following these patterns, you can build robust, secure, and maintainable backend systems. Remember: validate early, transform carefully, and never trust user input. 🛡️

Top comments (0)