JavaRush /Курсы /Модуль 3: React /Интеграция с REST API и создание сервисов

Интеграция с REST API и создание сервисов

Модуль 3: React
19 уровень , 3 лекция
Открыта

Добавим JWT в наше приложение

Приложение учёта расходов не имеет смысла без данных — пользователь должен видеть свои доходы и расходы, добавлять новые транзакции и управлять ими. Всё это работает через REST API, который связывает наш фронтенд (React) с сервером (бэкендом).

Работа с REST API — это необходимый навык фронтенд-разработчика. Вы будете использовать его:

  • при интеграции с внешними сервисами (например, платёжными системами);
  • при общении с внутренним бекендом (как в нашем проекте);
  • при подготовке к техническим собеседованиям (часто встречаются задачи на API-интеграции).

Основные термины REST API – освежим знания

REST — это способ организации API, который оперирует ресурсами и использует стандартные HTTP-методы:

HTTP Метод Назначение
GET Получить данные
POST Добавить новые данные
PUT/PATCH Обновить существующие данные
DELETE Удалить данные

Например:

  • GET /transactions — получить список всех транзакций;
  • POST /transactions — добавить новую;
  • DELETE /transactions/3 — удалить транзакцию с id = 3.

Установка Axios

Для взаимодействия с сервером мы используем Axios — популярную библиотеку, которая делает работу с HTTP-запросами удобнее, чем fetch.

Выполняем в терминале:

npm install axios

Настройка Axios

Создадим общий конфиг для всех запросов — это удобно для централизованной настройки, повторного использования и добавления заголовков, например токена авторизации.

Файл: src/services/api.ts

import axios from 'axios';

// Создаём экземпляр Axios с базовой конфигурацией
const api = axios.create({
  baseURL: 'https://your-api.example.com/api', // Замените на адрес своего сервера
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Добавляем интерцептор для авторизации: вставляем токен в каждый запрос
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token'); // Получаем токен из localStorage
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`; // Добавляем токен в заголовки
  }
  return config;
});

// Обработка ошибок: можно логировать или перенаправлять при 401
api.interceptors.response.use(
  (response) => response,
  (error) => {
    console.error('API Error:', error); // Здесь можно сделать глобальный toast, logout и т. д.
    return Promise.reject(error);
  }
);

export default api;

Объяснение:

  • Мы настраиваем базовый URL — чтобы каждый запрос не указывать его вручную.
  • Добавляем обработку токена — так мы сразу защищаем все запросы.
  • Перехватываем ошибки: можем централизованно реагировать на 401 (например, выйти из аккаунта).

Сервис для работы с транзакциями

Теперь создадим модуль, где будут все функции, связанные с транзакциями: загрузка, добавление, удаление.

Файл: src/services/transactionService.ts

import api from './api'; // Импортируем наш сконфигурированный Axios

// Интерфейс одной транзакции
export interface Transaction {
  id: number;
  type: 'income' | 'expense'; // Доход или расход
  category: string;           // Категория (например, "Еда")
  amount: number;             // Сумма
  note?: string;              // Необязательная заметка
  date: string;               // Дата в ISO формате
}

// Получить список всех транзакций
export const fetchTransactions = async (): Promise<Transaction[]> => {
  const res = await api.get('/transactions'); // GET-запрос
  return res.data;
};

// Добавить новую транзакцию
export const createTransaction = async (
  transaction: Omit<Transaction, 'id'> // Без id — сервер его сгенерирует
): Promise<Transaction> => {
  const res = await api.post('/transactions', transaction); // POST-запрос
  return res.data;
};

// Удалить транзакцию по id
export const deleteTransaction = async (id: number): Promise<void> => {
  await api.delete(`/transactions/${id}`); // DELETE-запрос
};

Ключевые моменты:

  • Все функции асинхронные.
  • Ответы типизированы, что делает работу безопаснее.
  • Используем Omit<Transaction, 'id'> — мы не передаём id при создании, он генерируется сервером.

Использование сервиса в компонентах

Компонент: отображение транзакций

Файл: src/components/TransactionList.tsx

import React, { useEffect, useState } from 'react';
import { fetchTransactions, Transaction } from '../services/transactionService';

const TransactionList: React.FC = () => {
  const [transactions, setTransactions] = useState<Transaction[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    // Загружаем транзакции при монтировании
    const load = async () => {
      try {
        const data = await fetchTransactions(); // Вызов сервиса
        setTransactions(data);
      } catch (err) {
        setError('Не удалось загрузить данные');
      } finally {
        setLoading(false);
      }
    };

    load();
  }, []);

  if (loading) return <p>Загрузка...</p>;
  if (error) return <p>{error}</p>;

  return (
    <ul>
      {transactions.map(tx => (
        <li key={tx.id}>
          {tx.date.slice(0, 10)} — <strong>{tx.category}</strong>: {tx.type === 'income' ? '+' : '-'}${tx.amount}
        </li>
      ))}
    </ul>
  );
};

export default TransactionList;

Ключевые моменты:

  • Сначала отображается спиннер Загрузка..., затем данные.
  • При ошибке пользователь увидит понятное сообщение.
  • Мы обрезаем дату slice(0, 10), чтобы показать только YYYY-MM-DD.

Компонент: форма добавления транзакции

Файл: src/components/AddTransactionForm.tsx

import React, { useState } from 'react';
import { createTransaction } from '../services/transactionService';

const AddTransactionForm: React.FC = () => {
  const [type, setType] = useState<'income' | 'expense'>('expense');
  const [amount, setAmount] = useState('');
  const [category, setCategory] = useState('');
  const [note, setNote] = useState('');

  const handleSubmit = async () => {
    if (!amount || !category) return;

    try {
      await createTransaction({
        type,
        category,
        amount: parseFloat(amount),
        note,
        date: new Date().toISOString(),
      });

      // Очищаем поля после успешной отправки
      setAmount('');
      setCategory('');
      setNote('');
    } catch (e) {
      alert('Ошибка при добавлении транзакции');
    }
  };

  return (
    <div>
      <h3>Добавить транзакцию</h3>
      <input
        type="number"
        value={amount}
        placeholder="Сумма"
        onChange={(e) => setAmount(e.target.value)}
      />
      <input
        type="text"
        value={category}
        placeholder="Категория"
        onChange={(e) => setCategory(e.target.value)}
      />
      <input
        type="text"
        value={note}
        placeholder="Заметка"
        onChange={(e) => setNote(e.target.value)}
      />
      <select value={type} onChange={(e) => setType(e.target.value as 'income' | 'expense')}>
        <option value="income">Доход</option>
        <option value="expense">Расход</option>
      </select>
      <button onClick={handleSubmit}>Добавить</button>
    </div>
  );
};

export default AddTransactionForm;

Что здесь важно:

  • Все поля управляются через useState.
  • Валидация: если поля не заполнены — транзакция не отправляется.
  • После добавления — поля очищаются, пользователь готов к следующему вводу.

Обработка ошибок и состояния загрузки

Тут всё просто, но важно! Никто не любит приложения, которые "зависают" без видимой причины. Поэтому:

  1. Добавляем индикаторы (например, "Загрузка...").
  2. Показываем пользовательские понятные сообщения об ошибках.

Если сервер недоступен, данные не загружаются или что-то пошло не так, пользователи должны понимать, что делать.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ