Встречал ли в работе дублирование логики между сервером и клиентом
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Встречал ли в работе дублирование логики между сервером и клиентом
Да, дублирование логики между сервером и клиентом — это частая и серьезная проблема в полностековой разработке. Это один из главных источников багов, так как логика может расходиться и приводить к несогласованности данных.
Типичные примеры дублирования
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 как черный ящик
- Документируй бизнес-правила в одном месте