← Назад к вопросам
Как сделать классический рендеринг при универсальном API для разных потребителей?
2.3 Middle🔥 141 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Универсальное API для разных клиентов
Это вопрос о том, как сделать один API, который подходит как для веб-браузеров (SSR, CSR), так и для нативных приложений, мобильных клиентов и других потребителей. Ключ — разделить логику рендеринга от логики доставки данных.
Архитектурный подход
┌─────────────────────────────────────────────────────┐
│ Backend API │
│ (данные, логика, валидация, авторизация) │
└──────────────────┬──────────────────────────────────┘
│
┌────────────┼────────────┬───────────┐
↓ ↓ ↓ ↓
Web SSR Web CSR Mobile Desktop
(Next.js) (React) (React Native) (Electron)
1. REST/GraphQL API — чистые данные
Первое правило: API должен возвращать только данные, без информации о рендеринге.
// ✅ ХОРОШО — API возвращает данные
GET /api/v1/products/123
{
"id": "123",
"name": "Ноутбук",
"price": 99999,
"description": "Мощный ноутбук",
"image_url": "https://cdn.example.com/product.jpg",
"rating": 4.5,
"in_stock": true,
"category_id": "electronics"
}
// ❌ ПЛОХО — API содержит информацию о рендеринге
GET /api/v1/products/123?render=web
{
"html": "<div class='product'>...</div>",
"css": ".product { color: blue; }",
"javascript": "console.log('loaded');"
}
2. Формирование ответа в зависимости от клиента
// Backend API
const express = require('express');
const app = express();
// Один endpoint, разные клиенты
app.get('/api/v1/products/:id', async (req, res) => {
const clientType = req.headers['x-client-type']; // 'web', 'mobile', 'desktop'
const data = await getProductData(req.params.id);
// Базовые данные для всех
let response = {
id: data.id,
name: data.name,
price: data.price,
description: data.description
};
// Дополнительные поля для разных клиентов
if (clientType === 'web') {
response.image_url = data.image_url;
response.thumbnail_url = data.thumbnail_url;
response.seo_title = data.seo_title;
response.seo_description = data.seo_description;
}
if (clientType === 'mobile') {
response.image_mobile = data.image_mobile;
response.rating = data.rating; // важнее для мобильных
}
if (clientType === 'desktop') {
response.detailed_images = data.detailed_images;
response.specifications = data.specifications;
}
res.json(response);
});
3. Server-Side Rendering (SSR) в Next.js
// pages/products/[id].tsx
import { GetServerSideProps } from 'next';
interface Product {
id: string;
name: string;
price: number;
description: string;
image_url: string;
}
interface Props {
product: Product;
}
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { id } = context.params as { id: string };
// Запрос к API с указанием типа клиента
const res = await fetch(`https://api.example.com/api/v1/products/${id}`, {
headers: {
'x-client-type': 'web', // указываем, что это веб-клиент
}
});
if (!res.ok) {
return { notFound: true };
}
const product = await res.json();
return {
props: { product },
revalidate: 3600 // ISR (Incremental Static Regeneration)
};
};
export default function ProductPage({ product }: Props) {
return (
<div className="product-detail">
<h1>{product.name}</h1>
<img src={product.image_url} alt={product.name} />
<p className="price">${product.price}</p>
<p>{product.description}</p>
</div>
);
}
4. Client-Side Rendering (CSR) в React
// ProductPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
interface Product {
id: string;
name: string;
price: number;
description: string;
image_url: string;
}
export function ProductPage() {
const { id } = useParams<{ id: string }>();
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchProduct();
}, [id]);
const fetchProduct = async () => {
try {
setLoading(true);
const res = await fetch(`/api/v1/products/${id}`, {
headers: {
'x-client-type': 'web'
}
});
if (!res.ok) throw new Error('Failed to load');
const data = await res.json();
setProduct(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!product) return <div>Not found</div>;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<img src={product.image_url} alt={product.name} />
<p className="price">${product.price}</p>
<p>{product.description}</p>
</div>
);
}
5. Мобильный клиент (React Native)
// screens/ProductScreen.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, Image, ScrollView, ActivityIndicator } from 'react-native';
interface Product {
id: string;
name: string;
price: number;
description: string;
image_mobile: string; // мобильная версия изображения
rating: number;
}
export function ProductScreen({ route }) {
const { id } = route.params;
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProduct();
}, [id]);
const fetchProduct = async () => {
try {
const res = await fetch(`https://api.example.com/api/v1/products/${id}`, {
headers: {
'x-client-type': 'mobile' // указываем мобильный клиент
}
});
const data = await res.json();
setProduct(data);
} finally {
setLoading(false);
}
};
if (loading) return <ActivityIndicator size="large" />;
if (!product) return <Text>Not found</Text>;
return (
<ScrollView>
<Image
source={{ uri: product.image_mobile }}
style={{ width: '100%', height: 300 }}
/>
<View style={{ padding: 16 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>
{product.name}
</Text>
<Text style={{ fontSize: 18, color: '#27ae60', marginVertical: 10 }}>
${product.price}
</Text>
<Text>{product.description}</Text>
<Text style={{ marginTop: 10 }}>Rating: {product.rating}⭐</Text>
</View>
</ScrollView>
);
}
6. Кэширование и оптимизация
// Frontend кэширование
const cache = new Map();
async function fetchWithCache(id: string, clientType: string) {
const cacheKey = `${id}-${clientType}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const res = await fetch(`/api/v1/products/${id}`, {
headers: { 'x-client-type': clientType }
});
const data = await res.json();
cache.set(cacheKey, data);
return data;
}
// Backend кэширование
const Redis = require('redis');
const redis = Redis.createClient();
app.get('/api/v1/products/:id', async (req, res) => {
const clientType = req.headers['x-client-type'];
const cacheKey = `product:${req.params.id}:${clientType}`;
// Проверяем кэш
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Получаем из БД
const data = await getProductData(req.params.id);
const response = formatResponse(data, clientType);
// Кэшируем на час
await redis.setex(cacheKey, 3600, JSON.stringify(response));
res.json(response);
});
7. Версионирование API
// Если нужны значительные изменения, создаём новую версию
GET /api/v1/products/:id // старая версия
GET /api/v2/products/:id // новая версия
// Или используем Accept header
GET /api/products/:id
Accept: application/vnd.example.v1+json
Accept: application/vnd.example.v2+json
Выводы
- Разделение ответственности — API отвечает за данные, клиент за рендеринг
- Content-negotiation — API возвращает разные данные для разных клиентов
- Единый источник истины — одна БД, один бэкенд для всех
- Кэширование — на обоих уровнях (frontend и backend)
- Версионирование — развивайте API без ломания старых клиентов
- SSR/CSR гибридность — Next.js и подобные фреймворки позволяют переключаться между подходами
Это позволяет масштабировать приложение и поддерживать разные типы клиентов эффективно.