← Назад к вопросам

Встречал ли в работе дублирование логики между сервером и клиентом

2.3 Middle🔥 202 комментариев
#JavaScript Core

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Встречал ли в работе дублирование логики между сервером и клиентом

Да, дублирование логики между сервером и клиентом — это частая и серьезная проблема в полностековой разработке. Это один из главных источников багов, так как логика может расходиться и приводить к несогласованности данных.

Типичные примеры дублирования

1. Валидация формы

Одна из самых частых ошибок — валидация только на клиенте:

// Frontend: валидация email
function validateEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function handleSubmit(formData) {
  if (!validateEmail(formData.email)) {
    setError('Invalid email');
    return;
  }
  apiCall.post('/register', formData);
}
# Backend: забыли про валидацию (БАГ!)
@app.post('/register')
def register(data):
  user = User(email=data.email)
  db.session.add(user)
  db.session.commit()

Это очень опасно:

  • Пользователь может обойти клиентскую валидацию (отключить JS)
  • В БД попадут невалидные данные
  • Потенциальная уязвимость безопасности

Правильный подход:

// Frontend: валидация для UX
function validateEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function handleSubmit(formData) {
  if (!validateEmail(formData.email)) {
    setError('Invalid email format');
    return;
  }
  apiCall.post('/register', formData);
}
# Backend: валидация обязательна
from pydantic import EmailStr, ValidationError

class RegisterRequest(BaseModel):
  email: EmailStr
  password: str

@app.post('/register')
def register(data: RegisterRequest):
  user = User(email=data.email)
  db.session.add(user)
  db.session.commit()

2. Форматирование данных

// Frontend: форматирование цены
function formatPrice(price) {
  return `$${(price / 100).toFixed(2)}`;
}
# Backend: разные правила
def get_product(product_id):
  product = db.get_product(product_id)
  return {'price': product.price / 100}

Результат: данные не совпадают.

3. Авторизация и правила доступа

// Frontend: скрываю кнопку если не admin
function ProductCard({ product, user }) {
  if (user.role !== 'admin') {
    return <div><h2>{product.name}</h2></div>;
  }
  
  return (
    <div>
      <h2>{product.name}</h2>
      <button onClick={() => deleteProduct(product.id)}>Delete</button>
    </div>
  );
}
# Backend: забыли про проверку (КРИТИЧЕСКИЙ БАГ!)
@app.delete('/products/{product_id}')
def delete_product(product_id):
  product = db.get_product(product_id)
  db.delete(product)
  return {'ok': True}

Пользователь может просто отправить DELETE запрос curl!

Правильный подход:

@app.delete('/products/{product_id}')
def delete_product(product_id, current_user: User = Depends(get_current_user)):
  if current_user.role != 'admin':
    raise HTTPException(status_code=403, detail='Forbidden')
  
  product = db.get_product(product_id)
  db.delete(product)
  return {'ok': True}

4. Вычисления скидок и цен

// Frontend: показываю скидку
function CartItem({ item }) {
  const discount = item.price * 0.2;
  const finalPrice = item.price - discount;
  
  return (
    <div>
      <p>Original: ${item.price}</p>
      <p>Final: ${finalPrice}</p>
    </div>
  );
}
# Backend: другие правила
def calculate_order_total(items):
  total = 0
  for item in items:
    discount = item.price * 0.1
    total += item.price - discount
  return total

Сервер и клиент считают по-разному!

Как я решал эту проблему

Подход 1: Shared TypeScript/JavaScript библиотеки

// packages/shared/validators/email.ts
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

Фронтенд и бэкенд используют одни и те же функции валидации.

Подход 2: OpenAPI для генерации кода

components:
  schemas:
    User:
      type: object
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8

Генерирую TypeScript типы и валидаторы из одного источника.

Подход 3: Server-driven UI

Отправляю правила валидации с сервера:

{
  "fields": [
    {
      "name": "email",
      "type": "email",
      "required": true,
      "pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
    }
  ]
}

Фронтенд использует эти правила для валидации.

Лучшие практики

1. НИКОГДА не доверяй клиенту

Всегда валидируй на сервере:

@app.post('/api/orders')
def create_order(order_data: OrderRequest, current_user: User = Depends(auth)):
  # 1. Проверяю авторизацию
  if not current_user:
    raise Unauthorized()
  
  # 2. Валидирую данные
  if not order_data.email:
    raise ValidationError('Email is required')
  
  # 3. Проверяю бизнес-правила
  if order_data.total < 0:
    raise ValueError('Total cannot be negative')
  
  # 4. Только тогда сохраняю
  order = Order.create(order_data)
  db.commit()

2. Клиент валидирует для UX, сервер — для безопасности

// Frontend: быстрая обратная связь
function handleEmailChange(email) {
  if (!isValidEmail(email)) {
    setEmailError('Invalid format');
  } else {
    setEmailError(null);
  }
}

// Но все равно отправляю на сервер
function handleSubmit(data) {
  apiCall.post('/register', data);
}

3. Документируй правила на одном месте

export const VALIDATION_RULES = {
  EMAIL: {
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    message: 'Invalid email format',
  },
  PASSWORD: {
    minLength: 8,
    message: 'Min 8 chars',
  },
};

Используй везде: frontend, backend, тесты.

4. Тестируй API как черный ящик

describe('API Security', () => {
  it('should reject invalid email', async () => {
    const response = await fetch('/api/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'not-an-email',
        password: 'Valid123',
      }),
    });
    
    expect(response.status).toBe(400);
  });
  
  it('should reject unauthorized delete', async () => {
    const response = await fetch('/api/products/123', {
      method: 'DELETE',
    });
    
    expect(response.status).toBe(403);
  });
});

Реальный кейс из практики

Работал над e-commerce платформой. Нашли баг:

  • Клиент считал цену с скидкой: price * 0.85
  • Сервер считал: price * 0.9
  • При заказе 10 товаров по 100 долларов:
    • Клиент показал: 850 долларов
    • Сервер зарядил: 900 долларов
    • Компания теряет 50 долларов за заказ

Решение: вся логика скидок только на сервере.

@app.get('/products/{product_id}')
def get_product(product_id, current_user=Depends(auth)):
  product = db.get_product(product_id)
  
  base_price = product.price
  discount = calculate_user_discount(current_user)
  final_price = base_price * (1 - discount)
  
  return {
    'id': product.id,
    'name': product.name,
    'basePrice': base_price,
    'discount': discount,
    'finalPrice': final_price,
  }
// Frontend: просто показываю
function ProductPage({ product }) {
  return (
    <div>
      <p>Price: ${product.basePrice}</p>
      <p>Final: ${product.finalPrice}</p>
    </div>
  );
}

Вывод

  • Дублирование логики — частая проблема, приводит к багам и потере денег
  • Клиент валидирует для UX, сервер валидирует для безопасности
  • Сервер — единственный источник истины для критичных операций
  • Используй shared libraries или code generation для синхронизации
  • Тестируй API как черный ящик
  • Документируй бизнес-правила в одном месте
Встречал ли в работе дублирование логики между сервером и клиентом | PrepBro