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

Как реализовать взаимодействие frontend и API?

1.0 Junior🔥 221 комментариев
#API и сетевые протоколы

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

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

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

Взаимодействие Frontend и Backend API

Это ключевой аспект разработки — правильно организованная коммуникация между фронтенд и бэкенд частями приложения. Рассмотрю полный цикл от проектирования до production.

1. Проектирование API (договор между сторонами)

Определение эндпоинтов

// API Specification (OpenAPI/Swagger)
// Это договор между frontend и backend

interface APIContract {
  // GET - получение данных
  'GET /api/v1/users': {
    query: { limit?: number; offset?: number; search?: string };
    response: {
      data: User[];
      total: number;
      hasMore: boolean;
    };
  };
  
  // GET с ID
  'GET /api/v1/users/:id': {
    params: { id: string };
    response: User | { error: string };
  };
  
  // POST - создание
  'POST /api/v1/users': {
    body: { name: string; email: string };
    response: User | { error: string };
  };
  
  // PUT - обновление
  'PUT /api/v1/users/:id': {
    params: { id: string };
    body: Partial<User>;
    response: User | { error: string };
  };
  
  // DELETE - удаление
  'DELETE /api/v1/users/:id': {
    params: { id: string };
    response: { success: boolean };
  };
}

Swagger документация

# Автоматическая генерация Swagger из NestJS
# frontend разработчик открывает http://localhost:3000/api/docs
# и видит все эндпоинты с примерами
// In NestJS
import { ApiOperation, ApiResponse } from '@nestjs/swagger';

@Controller('users')
export class UsersController {
  @Get()
  @ApiOperation({ summary: 'Get all users' })
  @ApiResponse({
    status: 200,
    description: 'List of users',
    type: [UserDto],
  })
  async getAll() {
    return this.usersService.findAll();
  }
}

2. Frontend запрос к API

Базовая структура (React/Next.js)

// hooks/useUsers.ts
import { useState, useEffect } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
}

export function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        // Запрос к backend API
        const response = await fetch(
          'http://localhost:3000/api/v1/users?limit=10',
          {
            method: 'GET',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${getToken()}`,
            },
          }
        );
        
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        
        const { data } = await response.json();
        setUsers(data);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
        setUsers([]);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUsers();
  }, []);
  
  return { users, loading, error };
}

// Component
export function UsersList() {
  const { users, loading, error } = useUsers();
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

API Client с Axios (лучший подход)

// api/client.ts
import axios, { AxiosInstance } from 'axios';

class APIClient {
  private client: AxiosInstance;
  
  constructor() {
    this.client = axios.create({
      baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
    });
    
    // Автоматически добавляем token в каждый запрос
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('authToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
    
    // Обработка ошибок
    this.client.interceptors.response.use(
      (response) => response.data,
      (error) => {
        if (error.response?.status === 401) {
          // Unauthorized - refresh token или logout
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }
  
  // Methods
  async getUsers(limit = 10, offset = 0) {
    return this.client.get('/api/v1/users', {
      params: { limit, offset },
    });
  }
  
  async getUser(id: string) {
    return this.client.get(`/api/v1/users/${id}`);
  }
  
  async createUser(data: { name: string; email: string }) {
    return this.client.post('/api/v1/users', data);
  }
  
  async updateUser(id: string, data: Partial<User>) {
    return this.client.put(`/api/v1/users/${id}`, data);
  }
  
  async deleteUser(id: string) {
    return this.client.delete(`/api/v1/users/${id}`);
  }
}

export const apiClient = new APIClient();

3. Backend обработка запроса

// users.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto } from './dto';

@Controller('api/v1/users')
export class UsersController {
  constructor(private usersService: UsersService) {}
  
  // GET /api/v1/users?limit=10&offset=0
  @Get()
  async getAll(
    @Query('limit') limit: number = 10,
    @Query('offset') offset: number = 0,
  ) {
    const [data, total] = await this.usersService.findAll(limit, offset);
    
    return {
      data,
      total,
      hasMore: offset + limit < total,
    };
  }
  
  // GET /api/v1/users/:id
  @Get(':id')
  async getOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }
  
  // POST /api/v1/users
  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
  
  // PUT /api/v1/users/:id
  @Put(':id')
  async update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }
  
  // DELETE /api/v1/users/:id
  @Delete(':id')
  async delete(@Param('id') id: string) {
    await this.usersService.delete(id);
    return { success: true };
  }
}

4. Обработка ошибок и валидация

Backend валидация

// dto/create-user.dto.ts
import { IsEmail, MinLength, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @MinLength(3)
  name: string;
  
  @IsNotEmpty()
  @IsEmail()
  email: string;
}

// Автоматическая валидация в контроллере
@Post()
@UsePipes(new ValidationPipe()) // Валидация DTO
async create(@Body() createUserDto: CreateUserDto) {
  // Если DTO невалидна, NestJS автоматически вернёт 400 с ошибками
  return this.usersService.create(createUserDto);
}

Frontend обработка ошибок

// api/client.ts
this.client.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 400) {
      // Validation error
      const { message, errors } = error.response.data;
      console.error('Validation errors:', errors);
      // Показать пользователю
      return Promise.reject(new ValidationError(errors));
    }
    
    if (error.response?.status === 401) {
      // Unauthorized
      redirectToLogin();
    }
    
    if (error.response?.status === 500) {
      // Server error
      showErrorNotification('Server error. Try again later');
    }
    
    return Promise.reject(error);
  }
);

5. Аутентификация и авторизация

Backend

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}
  
  async login(email: string, password: string) {
    const user = await this.validateUser(email, password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    
    // Генерируем токен
    const token = this.jwtService.sign({
      sub: user.id,
      email: user.email,
    });
    
    return { access_token: token };
  }
}

// Guard для защиты эндпоинтов
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}
  
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const authHeader = request.headers.authorization;
    
    if (!authHeader) {
      throw new UnauthorizedException('No token provided');
    }
    
    const [, token] = authHeader.split(' ');
    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

// Использование
@Controller('api/v1/users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  @Get('profile')
  getProfile(@Request() req) {
    return this.usersService.findOne(req.user.sub);
  }
}

Frontend

// hooks/useAuth.ts
export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Проверяем есть ли токен при загрузке
    const token = localStorage.getItem('authToken');
    if (token) {
      // Проверяем валидность токена
      apiClient.getProfile()
        .then(setUser)
        .catch(() => logout());
    }
    setLoading(false);
  }, []);
  
  const login = async (email: string, password: string) => {
    const { access_token } = await apiClient.login(email, password);
    localStorage.setItem('authToken', access_token);
    const user = await apiClient.getProfile();
    setUser(user);
  };
  
  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };
  
  return { user, loading, login, logout, isAuthenticated: !!user };
}

6. CORS (Cross-Origin Resource Sharing)

// Backend конфигурация
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // CORS для development
  app.enableCors({
    origin: process.env.FRONTEND_URL || 'http://localhost:3001',
    credentials: true, // Для cookies
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  });
  
  await app.listen(3000);
}

bootstrap();

7. Полный цикл запроса

// 1. Frontend делает запрос
const response = await apiClient.getUsers();

// 2. API Client добавляет headers и token
// GET /api/v1/users
// Headers: Authorization: Bearer token123

// 3. Backend получает запрос
// Middleware проверяет CORS
// Guard проверяет авторизацию
// Pipe валидирует параметры

// 4. Controller вызывает Service
const users = await this.usersService.findAll(limit, offset);

// 5. Service работает с БД
const [data, total] = await this.repository.findAndCount(...);

// 6. Возвращаем ответ
return { data, total, hasMore };

// 7. Frontend получает ответ
// Обновляет state
setUsers(response.data);

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

interface BestPractices {
  // Версионирование API
  urls: [
    '/api/v1/users',  // Текущая версия
    '/api/v2/users',  // Новая версия (обратная совместимость)
  ];
  
  // Консистентные коды ошибок
  errors: {
    400: 'Bad Request - Validation error',
    401: 'Unauthorized - No token or invalid',
    403: 'Forbidden - No permission',
    404: 'Not Found - Resource not exists',
    500: 'Internal Server Error',
  };
  
  // Структура ответа
  response: {
    success: true,
    data: {},
    error?: null,
    timestamp: '2024-01-01T10:00:00Z',
  };
  
  // Pagination для больших наборов
  pagination: {
    limit: 10,
    offset: 0,
    total: 100,
    hasMore: true,
  };
  
  // Rate limiting
  rateLimiting: '100 requests per minute',
}

Взаимодействие frontend и backend — это искусство согласования контрактов через API. Основная идея: четко определить что передаёт фронтенд, что возвращает бэкенд, и оба должны строго придерживаться этого контракта.