Добавим 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. - Валидация: если поля не заполнены — транзакция не отправляется.
- После добавления — поля очищаются, пользователь готов к следующему вводу.
Обработка ошибок и состояния загрузки
Тут всё просто, но важно! Никто не любит приложения, которые "зависают" без видимой причины. Поэтому:
- Добавляем индикаторы (например, "Загрузка...").
- Показываем пользовательские понятные сообщения об ошибках.
Если сервер недоступен, данные не загружаются или что-то пошло не так, пользователи должны понимать, что делать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ