Что такое API и зачем оно нужно
Представьте ресторан. Вы (клиент) сидите за столиком. На кухне - повара (сервер). Между вами - официант. Вы не идёте на кухню сами, а говорите официанту: "Мне, пожалуйста, борщ и чай". Официант передаёт заказ на кухню, кухня готовит, официант приносит вам еду.
API (Application Programming Interface) - это тот самый "официант" в мире программирования. Это набор правил, по которым одна программа общается с другой. Ваше мобильное приложение хочет показать список товаров? Оно отправляет запрос на API сервера, сервер достаёт товары из базы данных и отправляет их обратно.
Но вот вопрос: по каким именно правилам общаться? Как формулировать запросы? В каком формате получать ответы? Для этого придумали несколько подходов. Три самых популярных в 2026 году - это REST, GraphQL и tRPC. Давайте разберём каждый.
REST - "классика жанра"
REST (Representational State Transfer) - это самый распространённый подход к созданию API. Ему уже больше 20 лет, и он до сих пор работает отлично.
Идея REST простая: каждый "ресурс" (пользователь, товар, заказ) имеет свой адрес (URL), а действия с ним выполняются через HTTP-методы:
- GET - получить данные (как вопрос "покажи мне список товаров")
- POST - создать новые данные (как "добавь новый товар")
- PUT - обновить существующие данные ("измени цену товара #5")
- DELETE - удалить данные ("удали товар #5")
// REST API на Node.js + Express
// Express - это библиотека для создания веб-серверов на JavaScript
// Подключаем Express
import express from 'express'
const app = express()
// Говорим Express понимать JSON в теле запроса
// Без этой строки req.body будет undefined
app.use(express.json())
// Имитация базы данных (в реальном проекте здесь будет PostgreSQL или MySQL)
// В реальности товары хранятся в базе данных, не в переменной
let products = [
{ id: 1, name: 'Ноутбук', price: 79990, category: 'электроника' },
{ id: 2, name: 'Наушники', price: 4990, category: 'электроника' },
{ id: 3, name: 'Книга "JavaScript"', price: 890, category: 'книги' },
]
// --- GET /api/products - получить список всех товаров ---
// Когда кто-то отправляет GET-запрос на /api/products,
// выполняется эта функция
app.get('/api/products', (req, res) => {
// req.query - параметры из URL (после знака ?)
// Например: /api/products?category=электроника&sort=price
const { category, sort, limit } = req.query
let result = [...products] // Копируем массив (чтобы не менять оригинал)
// Фильтрация по категории (если указана)
if (category) {
result = result.filter(p => p.category === category)
}
// Сортировка (если указана)
if (sort === 'price') {
result.sort((a, b) => a.price - b.price)
}
// Ограничение количества (если указано)
if (limit) {
result = result.slice(0, parseInt(limit))
}
// res.json() - отправляем ответ в формате JSON
res.json({
data: result, // Массив товаров
total: result.length, // Сколько товаров в ответе
})
})
// --- GET /api/products/:id - получить один товар по ID ---
// :id - это "параметр пути". Если URL = /api/products/5, то req.params.id = "5"
app.get('/api/products/:id', (req, res) => {
// parseInt превращает строку "5" в число 5
const id = parseInt(req.params.id)
// Ищем товар с нужным ID
const product = products.find(p => p.id === id)
if (!product) {
// 404 = "не найдено"
// Это стандартный HTTP-код ошибки
return res.status(404).json({ error: 'Товар не найден' })
}
res.json({ data: product })
})
// --- POST /api/products - создать новый товар ---
// POST-запросы используются для создания новых данных
app.post('/api/products', (req, res) => {
// req.body - данные, которые отправил клиент в теле запроса
// Например: { "name": "Мышка", "price": 1990, "category": "электроника" }
const { name, price, category } = req.body
// Проверяем, что все обязательные поля заполнены
if (!name || !price) {
// 400 = "плохой запрос" (клиент отправил неправильные данные)
return res.status(400).json({ error: 'Укажите name и price' })
}
// Создаём новый товар
const newProduct = {
id: products.length + 1, // Генерируем ID (в реальности это делает БД)
name,
price: parseFloat(price), // Убеждаемся, что цена - число
category: category || 'без категории',
}
products.push(newProduct) // Добавляем в "базу данных"
// 201 = "создано" - стандартный код для успешного создания
res.status(201).json({ data: newProduct })
})
// --- PUT /api/products/:id - обновить товар ---
app.put('/api/products/:id', (req, res) => {
const id = parseInt(req.params.id)
const index = products.findIndex(p => p.id === id)
if (index === -1) {
return res.status(404).json({ error: 'Товар не найден' })
}
// Обновляем только те поля, которые были отправлены
// Если клиент отправил только { price: 5990 }, name не изменится
products[index] = { ...products[index], ...req.body }
res.json({ data: products[index] })
})
// --- DELETE /api/products/:id - удалить товар ---
app.delete('/api/products/:id', (req, res) => {
const id = parseInt(req.params.id)
const index = products.findIndex(p => p.id === id)
if (index === -1) {
return res.status(404).json({ error: 'Товар не найден' })
}
products.splice(index, 1) // Удаляем из массива
// 204 = "нет содержимого" - успешно удалено, нечего возвращать
res.status(204).send()
})
// Запускаем сервер на порту 3000
app.listen(3000, () => {
console.log('REST API сервер запущен: http://localhost:3000')
})
Как пользоваться этим API? Вот примеры запросов:
// Примеры запросов к REST API из браузера (JavaScript)
// 1. Получить все товары
const response = await fetch('/api/products')
const data = await response.json()
console.log(data)
// { data: [{id: 1, name: "Ноутбук", ...}, ...], total: 3 }
// 2. Получить товары только из категории "электроника"
const electronics = await fetch('/api/products?category=электроника')
// 3. Получить один товар по ID
const product = await fetch('/api/products/1')
// 4. Создать новый товар
const newProduct = await fetch('/api/products', {
method: 'POST', // Метод - POST (создание)
headers: { 'Content-Type': 'application/json' }, // Формат данных - JSON
body: JSON.stringify({ // Данные нового товара
name: 'Мышка беспроводная',
price: 1990,
category: 'электроника'
})
})
// 5. Обновить цену товара
await fetch('/api/products/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ price: 74990 }) // Меняем только цену
})
// 6. Удалить товар
await fetch('/api/products/3', { method: 'DELETE' })
Проблемы REST
REST отлично работает для простых случаев, но у него есть два известных недостатка:
Проблема 1: Избыточные данные (overfetching)
Допустим, вам нужно показать список товаров - только название и цену. Но API возвращает ВСЕ поля: id, name, price, category, description, images, reviews, specifications... Вы получаете в 10 раз больше данных, чем нужно.
Проблема 2: Множественные запросы (underfetching)
Допустим, вы открываете страницу заказа. Вам нужно: данные заказа + данные покупателя + список товаров в заказе. В REST это три отдельных запроса:
// Три запроса для одной страницы - это медленно!
// Каждый запрос - это "путешествие" данных через интернет
const order = await fetch('/api/orders/123')
const customer = await fetch('/api/customers/456')
const items = await fetch('/api/orders/123/items')
Эти проблемы и привели к созданию GraphQL.
GraphQL - запрашиваем только то, что нужно
GraphQL (Graph Query Language) придумали в Facebook в 2012 году. Их мобильное приложение отправляло кучу запросов и получало горы ненужных данных. Они решили: "А что если клиент сам будет указывать, какие данные ему нужны?"
Аналогия: REST - это меню ресторана с фиксированными блюдами. GraphQL - это шведский стол, где вы сами набираете то, что хотите.
// GraphQL - серверная часть
// Устанавливаем: npm install @apollo/server graphql
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
// Схема - описание всех типов данных и операций
// Это как "меню" вашего API: что можно запросить и что получить
const typeDefs = `#graphql
# Тип "Товар" - описывает структуру данных товара
# Каждое поле имеет тип: String (текст), Int (целое число), Float (дробное)
# ! означает "обязательное поле" (не может быть null)
type Product {
id: ID! # Уникальный идентификатор (обязательный)
name: String! # Название (обязательное)
price: Float! # Цена (обязательная)
category: String # Категория (необязательная, может быть null)
description: String # Описание
reviews: [Review!] # Список отзывов (массив объектов Review)
}
# Тип "Отзыв"
type Review {
id: ID!
text: String!
rating: Int! # Оценка от 1 до 5
author: String!
}
# Тип "Заказ" - содержит товары и информацию о покупателе
type Order {
id: ID!
customer: Customer!
items: [OrderItem!]!
total: Float!
status: String!
}
type Customer {
id: ID!
name: String!
email: String!
}
type OrderItem {
product: Product!
quantity: Int!
}
# Query - запросы на ЧТЕНИЕ данных (аналог GET в REST)
type Query {
products(category: String, limit: Int): [Product!]!
product(id: ID!): Product
order(id: ID!): Order
}
# Mutation - запросы на ИЗМЕНЕНИЕ данных (аналог POST/PUT/DELETE в REST)
type Mutation {
createProduct(name: String!, price: Float!, category: String): Product!
updateProduct(id: ID!, name: String, price: Float): Product
deleteProduct(id: ID!): Boolean!
}
`
// Резолверы - функции, которые обрабатывают запросы
// Для каждого поля в схеме нужен резолвер
const resolvers = {
Query: {
// Запрос списка товаров
products: (_, { category, limit }) => {
let result = [...products]
if (category) result = result.filter(p => p.category === category)
if (limit) result = result.slice(0, limit)
return result
},
// Запрос одного товара
product: (_, { id }) => products.find(p => p.id === id),
// Запрос заказа (со всеми связанными данными)
order: async (_, { id }) => {
const order = await db.query('SELECT * FROM orders WHERE id = ?', [id])
return order
},
},
// Резолвер для поля "reviews" в типе Product
// Вызывается ТОЛЬКО если клиент запросил отзывы
Product: {
reviews: (product) => {
return db.query('SELECT * FROM reviews WHERE product_id = ?', [product.id])
}
},
// Резолвер для поля "customer" в типе Order
Order: {
customer: (order) => {
return db.query('SELECT * FROM customers WHERE id = ?', [order.customerId])
},
items: (order) => {
return db.query('SELECT * FROM order_items WHERE order_id = ?', [order.id])
},
},
Mutation: {
createProduct: (_, { name, price, category }) => {
const newProduct = { id: Date.now().toString(), name, price, category }
products.push(newProduct)
return newProduct
},
deleteProduct: (_, { id }) => {
const index = products.findIndex(p => p.id === id)
if (index === -1) return false
products.splice(index, 1)
return true
},
},
}
// Создаём и запускаем сервер
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } })
console.log(`GraphQL API: ${url}`)
А вот как клиент использует GraphQL:
// Клиент GraphQL - запрашиваем ТОЛЬКО нужные поля
// Все запросы отправляются на один URL: /graphql
// И все через метод POST
// Запрос 1: Только названия и цены товаров (без описаний, отзывов и т.д.)
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `{
products {
name
price
}
}`
})
})
// Ответ: { data: { products: [{ name: "Ноутбук", price: 79990 }, ...] } }
// Только name и price - ничего лишнего!
// Запрос 2: Страница заказа - ВСЕ данные ОДНИМ запросом!
// Помните, в REST нужно было 3 отдельных запроса?
// В GraphQL - один:
const orderPage = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `{
order(id: "123") {
id
status
total
customer {
name
email
}
items {
quantity
product {
name
price
}
}
}
}`
})
})
// Один запрос - все данные для страницы!
// Сервер сам соберёт информацию из разных таблиц
tRPC - типобезопасность от клиента до сервера
tRPC (TypeScript Remote Procedure Call) - самый молодой из трёх подходов. Он появился в мире TypeScript и решает проблему, которая знакома каждому разработчику: клиент и сервер "не знают" друг о друге.
Аналогия: REST и GraphQL - это как общение по телефону. Вы надеетесь, что собеседник вас правильно понял. tRPC - это как телепатия: вы ТОЧНО знаете, что сервер ожидает и что вернёт, потому что типы данных общие.
// tRPC - серверная часть
// Устанавливаем: npm install @trpc/server @trpc/client zod
import { initTRPC } from '@trpc/server'
import { z } from 'zod' // Zod - библиотека для проверки данных
// Инициализируем tRPC
const t = initTRPC.create()
// Создаём "роутер" - набор всех функций API
const appRouter = t.router({
// "products" - группа функций для работы с товарами
products: t.router({
// Получить список товаров
// .input() описывает, какие параметры принимает функция
// z.object() - это "схема" входных данных
list: t.procedure
.input(z.object({
category: z.string().optional(), // category - необязательный текст
limit: z.number().min(1).max(100).optional(), // limit - число от 1 до 100
}).optional())
.query(async ({ input }) => {
// .query() - означает "чтение данных" (аналог GET)
// input - проверенные данные (если не прошли проверку - ошибка автоматически)
let result = await db.products.findMany()
if (input?.category) {
result = result.filter(p => p.category === input.category)
}
if (input?.limit) {
result = result.slice(0, input.limit)
}
return result
}),
// Получить один товар
byId: t.procedure
.input(z.object({
id: z.string() // id - обязательная строка
}))
.query(async ({ input }) => {
const product = await db.products.findUnique({ where: { id: input.id } })
if (!product) throw new Error('Товар не найден')
return product
}),
// Создать товар
create: t.procedure
.input(z.object({
name: z.string().min(1).max(200), // Название: от 1 до 200 символов
price: z.number().positive(), // Цена: положительное число
category: z.string().optional(),
}))
.mutation(async ({ input }) => {
// .mutation() - означает "изменение данных" (аналог POST/PUT/DELETE)
return await db.products.create({ data: input })
}),
// Удалить товар
delete: t.procedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
await db.products.delete({ where: { id: input.id } })
return { success: true }
}),
}),
})
// Экспортируем ТИП роутера - это ключевая фича tRPC!
// Клиент импортирует этот тип и ЗНАЕТ все функции API, их параметры и ответы
export type AppRouter = typeof appRouter
// tRPC - клиентская часть (React)
// МАГИЯ: клиент знает ВСЕ функции сервера, их параметры и типы ответов!
// Если сервер изменит API - клиент покажет ошибку ещё ДО запуска кода
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '../server/router' // Импорт ТИПА с сервера
// Создаём типизированный клиент
const trpc = createTRPCReact()
function ProductList() {
// trpc.products.list.useQuery() - вызов функции сервера
// TypeScript знает, что data - это массив товаров с полями id, name, price, ...
// Автокомплит работает! Если ошибётесь в имени поля - увидите ошибку в редакторе
const { data, isLoading, error } = trpc.products.list.useQuery({
category: 'электроника',
limit: 10
})
if (isLoading) return Загрузка...
if (error) return Ошибка: {error.message}
return (
{data.map(product => (
// TypeScript знает, что у product есть поля name и price
// Если напишете product.nmae - увидите ошибку!
-
{product.name} - {product.price} руб.
))}
)
}
function CreateProductForm() {
// useMutation - для создания/изменения данных
const createProduct = trpc.products.create.useMutation()
const handleSubmit = (formData) => {
// TypeScript проверит, что вы передали все нужные поля
// Если забудете price - редактор покажет ошибку ещё до запуска
createProduct.mutate({
name: formData.name,
price: parseFloat(formData.price),
category: formData.category,
})
}
// ...
}
Сравнение: что когда использовать
| Критерий | REST | GraphQL | tRPC |
|---|---|---|---|
| Сложность настройки | Простая | Средняя | Простая |
| Типобезопасность | Нет (нужна отдельная генерация) | Частичная (codegen) | Полная (автоматическая) |
| Гибкость запросов | Фиксированная структура | Клиент выбирает поля | Фиксированная структура |
| Публичный API | Отлично подходит | Отлично подходит | Не подходит (только TypeScript) |
| Производительность | Хорошая | Хорошая (но сложнее оптимизировать) | Отличная (минимум overhead) |
| Экосистема | Огромная | Большая | Растущая |
| Кэширование | Встроенное (HTTP-кэш) | Сложнее (один URL) | Через React Query |
Конкретные рекомендации
Выбирайте REST, если:
- Ваш API будет использоваться внешними разработчиками (публичный API)
- Команда не знакома с GraphQL или tRPC
- Простое CRUD-приложение без сложных связей между данными
- Нужна максимальная совместимость (REST работает с любым языком)
- Хотите использовать HTTP-кэширование
Выбирайте GraphQL, если:
- Много связанных данных (пользователи, заказы, товары, отзывы - всё связано)
- Разные клиенты (мобильное приложение, веб-сайт, десктоп) нуждаются в разных наборах данных
- Проблема N+1 запросов (одна страница требует 5-10 REST-запросов)
- Публичный API с богатой структурой данных
Выбирайте tRPC, если:
- И клиент, и сервер написаны на TypeScript
- Внутренний API (не публичный)
- Хотите максимальную типобезопасность и автокомплит
- Next.js или другой TypeScript-фреймворк
- Маленькая команда, где все пишут и фронт, и бэк
Версионирование API: как обновлять, не ломая
Когда API используют другие разработчики (или ваше мобильное приложение), нельзя просто взять и изменить формат ответа - всё сломается. Для этого используют версионирование:
// REST: версионирование через URL
// Старая версия API остаётся работать
// Новая живёт по другому адресу
// Версия 1 (старая, для существующих клиентов)
app.get('/api/v1/products', (req, res) => {
// Возвращает данные в старом формате
res.json(products.map(p => ({
id: p.id,
name: p.name,
price: p.price, // Цена в рублях (число)
})))
})
// Версия 2 (новая, с улучшениями)
app.get('/api/v2/products', (req, res) => {
// Возвращает данные в новом формате
res.json(products.map(p => ({
id: p.id,
name: p.name,
price: {
amount: p.price, // Цена (число)
currency: 'RUB', // Валюта (строка)
formatted: `${p.price} руб.` // Отформатированная цена
},
// Новое поле, которого не было в v1
images: p.images || [],
})))
})
// Клиенты старой версии продолжают работать
// Новые клиенты используют v2 с расширенными данными
Обработка ошибок: как правильно
Хорошее API всегда возвращает понятные ошибки. Не просто "Ошибка", а конкретно: что пошло не так и что делать.
// Правильная обработка ошибок в REST API
// Коды ошибок HTTP (должен знать каждый):
// 200 - Всё хорошо (OK)
// 201 - Создано (Created)
// 204 - Удалено, нечего возвращать (No Content)
// 400 - Плохой запрос (клиент отправил что-то не то)
// 401 - Не авторизован (не залогинен)
// 403 - Запрещено (залогинен, но нет прав)
// 404 - Не найдено
// 429 - Слишком много запросов (rate limit)
// 500 - Ошибка сервера (наша проблема)
// Пример: единообразная обработка ошибок
app.post('/api/orders', async (req, res) => {
try {
const { productId, quantity } = req.body
// Проверяем входные данные
if (!productId) {
return res.status(400).json({
error: 'MISSING_FIELD', // Код ошибки (для программы)
message: 'Укажите productId', // Описание (для человека)
field: 'productId', // Какое поле проблемное
})
}
// Проверяем, что товар существует
const product = await db.products.findById(productId)
if (!product) {
return res.status(404).json({
error: 'NOT_FOUND',
message: `Товар с ID ${productId} не найден`,
})
}
// Проверяем наличие на складе
if (product.stock < quantity) {
return res.status(400).json({
error: 'OUT_OF_STOCK',
message: `На складе только ${product.stock} шт., вы запросили ${quantity}`,
})
}
// Всё в порядке - создаём заказ
const order = await db.orders.create({ productId, quantity })
res.status(201).json({ data: order })
} catch (error) {
// Непредвиденная ошибка - логируем и возвращаем 500
console.error('Ошибка при создании заказа:', error)
res.status(500).json({
error: 'INTERNAL_ERROR',
message: 'Произошла ошибка на сервере. Попробуйте позже.',
})
}
})
Итого
REST, GraphQL и tRPC - это три подхода к одной задаче: как клиенту и серверу обмениваться данными. REST - проверенная классика, которая подходит для большинства случаев. GraphQL - гибкий инструмент для сложных приложений с множеством связей. tRPC - идеальный выбор для TypeScript-проектов, где и фронтенд, и бэкенд пишет одна команда.
Для большинства проектов начните с REST - он прост, понятен и хорошо документирован. Переходите на GraphQL или tRPC, когда REST начнёт мешать (много запросов на одну страницу, проблемы с типами, сложные связи данных). Нужна помощь с проектированием API для вашего проекта? Напишите нам - спроектируем архитектуру, которая будет масштабироваться вместе с вашим бизнесом.