Как сделать API для своего проекта

API — это слой, через который frontend, мобильное приложение или другой сервис общается с backend. В статье разберём, как подойти к созданию API без хаоса: от идеи и структуры до маршрутов, статус-кодов, валидации и практического примера на Node.js.

Обычно желание “сделать API” появляется в тот момент, когда проект перестаёт быть просто красивой страницей. Пока у нас лендинг, карточки товаров или статичный блог, можно жить на HTML, CSS и немного JavaScript. Но как только появляются пользователи, корзина, личный кабинет, каталог, фильтры, заказы или сохранение данных — frontend уже не справляется один.

Вот здесь и появляется API.

API можно представить как официанта между клиентом и кухней. Пользователь нажал кнопку “Добавить в корзину”, frontend передал запрос, backend обработал его, сходил в базу данных и вернул результат. Пользователь не знает, какие там таблицы, проверки и бизнес-логика. И не должен знать. Он просто видит, что товар добавился.

На практике API — это не “магическая backend-штука”, а набор понятных адресов, правил и ответов. Например, frontend спрашивает: “Дай мне список сборок ПК до 70 000 рублей”. API отвечает JSON-данными. Или frontend говорит: “Создай заказ”. API проверяет данные, сохраняет заказ и возвращает статус.

Главная ошибка новичков — сразу садиться писать код, не понимая, какие действия вообще должен выполнять проект. В итоге появляются странные URL, дублирующаяся логика, непонятные ответы и backend, который через неделю страшно открыть. Было? Было. И не раз.

С чего начинается API

API начинается не с Express, Nest.js, Django или Laravel. Оно начинается с вопроса: какие данные нужны проекту и что пользователь с ними делает?

Допустим, мы делаем сервис подбора игровых ПК. В нём могут быть готовые сборки, комплектующие, пользователи, избранное и заявки. Уже из этого можно выделить сущности. Сущность — это объект, с которым работает проект. Например, сборка ПК, видеокарта, процессор или пользователь.

Дальше мы думаем не о кнопках на сайте, а о действиях. Пользователь может получить список сборок, открыть одну сборку, создать свою, обновить её или удалить. Эти действия почти идеально ложатся на REST API.

REST — это подход, где мы работаем с ресурсами через HTTP-методы. Не надо усложнять. Если frontend хочет получить данные — чаще всего используется GET. Если нужно создать что-то новое — POST. Если изменить часть данных — PATCH. Если удалить — DELETE.

В нормальном API адрес должен читаться почти как обычная фраза:

GET /api/builds
GET /api/builds/15
POST /api/builds
PATCH /api/builds/15
DELETE /api/builds/15

Сразу видно, что речь идёт о сборках ПК. Не о загадочном /getData, не о /new-item-final-2, не о /test-api. Такие названия иногда появляются “временно”, а потом живут в проекте годами. Временно в разработке — это почти всегда навсегда, просто с чувством вины.

Как продумать структуру API

Перед кодом лучше набросать небольшую карту API. Не огромную документацию на 40 страниц, а обычную таблицу в голове или в заметках: какой URL, какой метод, что принимает и что возвращает.

Например, для проекта со сборками ПК можно начать так:

GET /api/builds

Этот маршрут возвращает список сборок. У него могут быть query-параметры:

GET /api/builds?maxBudget=70000&purpose=games

Здесь frontend просит сборки до 70 000 рублей для игр. Это удобнее, чем плодить отдельные адреса вроде /api/builds-under-70000-for-games. Чем больше таких “специальных” URL, тем быстрее API превращается в склад странных решений.

Для конкретной сборки используем route parameter:

GET /api/builds/15

Число 15 — это id сборки. API должен найти её и вернуть. Если сборки нет, нормальный ответ — 404 Not Found, а не пустой объект, не 200 OK с текстом “ошибка”, и точно не HTML-страница с криком сервера.

Статус-коды — это важная часть API. Frontend по ним понимает, что произошло. 200 — всё хорошо. 201 — создано. 400 — клиент отправил плохие данные. 401 — пользователь не авторизован. 403 — авторизован, но прав нет. 404 — ресурс не найден. 500 — что-то сломалось на сервере.

Звучит скучно, но именно на таких мелочах потом держится нормальная разработка. Когда API честно сообщает, что случилось, frontend не приходится гадать по кофейной гуще.

Пример API на Node.js и Express

Для примера возьмём простой backend на Node.js, Express и TypeScript. Можно написать и на чистом JavaScript, логика будет такой же. TypeScript здесь полезен тем, что помогает раньше заметить ошибки в структуре данных. Не спасает от всего, конечно, но хотя бы часть граблей убирает с дороги.

Установим зависимости:

npm install express cors zod
npm install -D typescript tsx @types/express @types/cors

express нужен для сервера и маршрутов, cors — чтобы frontend мог обращаться к API с другого адреса, а zod — для валидации входящих данных. Валидация — это не украшение, а обязательная вещь. Пользователь, браузер или другой сервис могут отправить что угодно. Backend должен быть к этому готов.

Создадим файл src/server.ts:

import express, { Request, Response } from "express";
import cors from "cors";
import { z } from "zod";

const app = express();

app.use(cors({
  origin: "http://localhost:5173"
}));

app.use(express.json());

type PcBuild = {
  id: number;
  title: string;
  budget: number;
  purpose: "games" | "work" | "universal";
  parts: {
    cpu: string;
    gpu: string;
    ram: string;
    storage: string;
  };
};

let builds: PcBuild[] = [
  {
    id: 1,
    title: "Бюджетная игровая сборка",
    budget: 45000,
    purpose: "games",
    parts: {
      cpu: "Xeon E5-2667 v4",
      gpu: "RX 580",
      ram: "16 GB DDR4",
      storage: "512 GB SSD"
    }
  },
  {
    id: 2,
    title: "Сбалансированная сборка для игр",
    budget: 75000,
    purpose: "games",
    parts: {
      cpu: "Intel Core i5-12400F",
      gpu: "RTX 3060",
      ram: "16 GB DDR4",
      storage: "1 TB SSD"
    }
  }
];

app.get("/api/health", (req: Request, res: Response) => {
  res.json({
    status: "ok",
    message: "API работает"
  });
});

app.get("/api/builds", (req: Request, res: Response) => {
  const maxBudget = req.query.maxBudget
    ? Number(req.query.maxBudget)
    : null;

  const purpose = req.query.purpose as PcBuild["purpose"] | undefined;

  let result = builds;

  if (maxBudget) {
    result = result.filter((build) => build.budget <= maxBudget);
  }

  if (purpose) {
    result = result.filter((build) => build.purpose === purpose);
  }

  res.json({
    data: result,
    meta: {
      count: result.length
    }
  });
});

app.get("/api/builds/:id", (req: Request, res: Response) => {
  const id = Number(req.params.id);

  const build = builds.find((item) => item.id === id);

  if (!build) {
    return res.status(404).json({
      message: "Сборка не найдена"
    });
  }

  res.json({
    data: build
  });
});

const createBuildSchema = z.object({
  title: z.string().min(3),
  budget: z.number().positive(),
  purpose: z.enum(["games", "work", "universal"]),
  parts: z.object({
    cpu: z.string().min(2),
    gpu: z.string().min(2),
    ram: z.string().min(2),
    storage: z.string().min(2)
  })
});

app.post("/api/builds", (req: Request, res: Response) => {
  const validation = createBuildSchema.safeParse(req.body);

  if (!validation.success) {
    return res.status(400).json({
      message: "Некорректные данные сборки",
      errors: validation.error.flatten()
    });
  }

  const newBuild: PcBuild = {
    id: Date.now(),
    ...validation.data
  };

  builds.push(newBuild);

  res.status(201).json({
    message: "Сборка создана",
    data: newBuild
  });
});

app.listen(3000, () => {
  console.log("API запущено на http://localhost:3000");
});

Пока данные хранятся в массиве. Для учебного примера это нормально. В реальном проекте вместо массива будет база данных: PostgreSQL, MongoDB, MySQL или что-то ещё. Но суть API от этого не меняется. Frontend всё равно обращается к маршрутам, backend всё равно проверяет данные, выполняет бизнес-логику и возвращает ответ.

Обрати внимание на маршрут /api/health. Он кажется мелочью, но в работе полезен. По нему можно быстро проверить, жив ли сервер. Когда проект разрастается, такие простые вещи экономят нервы. Особенно когда непонятно, проблема во frontend, backend, базе, Docker или “я просто не тот терминал открыл”.

Как frontend обращается к API

API само по себе пользователю не видно. Его обычно использует frontend. Например, страница каталога сборок может запросить данные так:

async function loadBuilds() {
  const response = await fetch(
    "http://localhost:3000/api/builds?maxBudget=70000&purpose=games"
  );

  if (!response.ok) {
    throw new Error("Не удалось загрузить сборки");
  }

  const result = await response.json();

  return result.data;
}

loadBuilds()
  .then((builds) => {
    console.log("Полученные сборки:", builds);
  })
  .catch((error) => {
    console.error(error.message);
  });

Здесь frontend не знает, как именно backend фильтрует сборки. Он просто отправляет запрос и получает результат. Это и есть нормальное разделение ответственности.

Frontend отвечает за интерфейс: кнопки, формы, страницы, состояние, отображение ошибок. Backend отвечает за данные, правила, безопасность, работу с базой и проверку входящих запросов.

Когда эти роли смешиваются, начинаются проблемы. Например, если вся логика проверки бюджета живёт только на frontend, пользователь может открыть DevTools и отправить другой запрос напрямую. Поэтому важные проверки всегда должны быть на backend. Frontend может помогать пользователю, но не должен быть единственным охранником двери.

Что должно быть в нормальном API

Хорошее API не обязательно должно быть сложным. Чаще наоборот: чем понятнее API, тем проще его развивать. В нормальном API маршруты называются предсказуемо, данные возвращаются в одном стиле, ошибки выглядят одинаково, а статус-коды соответствуют ситуации.

Например, если сборка не найдена, лучше вернуть такой ответ:

{
  "message": "Сборка не найдена"
}

А если данные не прошли валидацию:

{
  "message": "Некорректные данные сборки",
  "errors": {
    "fieldErrors": {
      "title": ["String must contain at least 3 character(s)"]
    }
  }
}

Да, текст ошибки от библиотеки можно потом привести к более дружелюбному виду. Но даже так это лучше, чем просто 500 Internal Server Error, когда пользователь забыл передать название.

Отдельно стоит подумать о формате успешных ответов. Например, можно всегда возвращать данные внутри поля data, а дополнительную информацию — внутри meta. Это удобно для списков, пагинации и фильтров.

{
  "data": [
    {
      "id": 1,
      "title": "Бюджетная игровая сборка",
      "budget": 45000
    }
  ],
  "meta": {
    "count": 1
  }
}

Такой подход не обязателен, но он дисциплинирует. Когда каждый маршрут отвечает как попало, frontend-разработчик начинает писать костыли под каждый отдельный случай. Сегодня это кажется мелочью, а завтра у тебя 20 разных обработчиков ошибок и тихая ненависть к собственному API.

Где здесь база данных

В примере выше мы использовали массив, но реальный API почти всегда работает с базой данных. Если проекту нужны строгие связи, заказы, пользователи, платежи и понятная структура, часто выбирают PostgreSQL. Если данные гибкие, например разные характеристики товаров или событий, иногда удобнее MongoDB. Но выбор базы — это отдельная тема.

Важно другое: API не должен отдавать наружу внутреннюю структуру базы как есть. База может хранить данные одним способом, а API возвращать их в более удобном виде для frontend.

Например, в базе у сборки могут быть отдельные таблицы builds, processors, graphics_cards, ram_modules. Но frontend не обязан собирать всё по кускам. API может вернуть уже готовую структуру:

{
  "id": 15,
  "title": "Игровая сборка до 70 000 рублей",
  "parts": {
    "cpu": "Intel Core i5-12400F",
    "gpu": "RTX 3060",
    "ram": "16 GB DDR4",
    "storage": "1 TB SSD"
  }
}

Это нормальная практика. API должен быть удобным не для базы данных, а для того, кто им пользуется. Чаще всего — для frontend.

Типичные ошибки при создании API

Самая частая ошибка — писать маршруты без системы. Сегодня /api/getUsers, завтра /api/user-list, послезавтра /api/all_users_final. Вроде работает, но проект быстро становится неприятным. Лучше сразу выбрать стиль и придерживаться его.

Вторая ошибка — отсутствие валидации. Backend получает title, ожидает строку, а ему приходит число, пустота или объект. Если данные не проверять, ошибка вылезет где-то глубже: в базе, в бизнес-логике или вообще у пользователя на странице. Чем раньше API отсекает плохой запрос, тем проще жить.

Третья ошибка — хранить всю логику прямо в маршрутах. Для маленького примера это нормально, но в реальном проекте лучше разделять код. Маршрут принимает запрос, контроллер обрабатывает HTTP-часть, сервис содержит бизнес-логику, репозиторий работает с базой. Не надо строить космическую архитектуру на три страницы кода, но и складывать всё в один файл server.js тоже сомнительное удовольствие.

Четвёртая ошибка — забывать про безопасность. Если API работает с пользователями, заказами, личными данными или админкой, нужна авторизация. Обычно это JWT-токены, сессии или внешние провайдеры. И важно помнить: если пользователь не должен видеть чужой заказ, это проверяется на backend. Не на frontend. Спрятанная кнопка — это не защита.

Пятая ошибка — не документировать API. Документация может быть простой: README, список маршрутов, примеры запросов и ответов. Для более серьёзных проектов можно подключить OpenAPI/Swagger. Документация нужна не “для красоты”, а чтобы через месяц не вспоминать, почему purpose принимает games, а не gaming.

Как развивать API дальше

Когда базовый API уже работает, можно двигаться дальше постепенно. Сначала подключить базу данных. Потом добавить авторизацию. После этого — роли пользователей, загрузку файлов, пагинацию, сортировку, логирование ошибок и тесты.

Не нужно пытаться сразу написать “идеальный backend”. Обычно это заканчивается тем, что человек три дня проектирует архитектуру, а потом не может вывести список товаров. Лучше сделать простой рабочий API, понять поток данных, а потом улучшать.

Для проекта среднего уровня хороший следующий шаг — вынести код по слоям:

src/
  server.ts
  routes/
    builds.routes.ts
  controllers/
    builds.controller.ts
  services/
    builds.service.ts
  repositories/
    builds.repository.ts
  schemas/
    build.schema.ts

Такая структура уже ближе к реальной разработке. Маршруты не превращаются в помойку, бизнес-логика лежит отдельно, схемы валидации можно переиспользовать, а работу с базой проще заменить или протестировать.

Если проект растёт дальше, можно смотреть в сторону Nest.js. Он более строгий, чем Express, и сначала кажется тяжеловатым. Но для больших backend-проектов структура Nest.js часто помогает не утонуть. Express хорош для старта и небольших сервисов, Nest.js — когда нужна дисциплина, модули, DI и более предсказуемая архитектура.

Вывод

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

Хорошее API должно быть понятным. Адреса читаются логично, HTTP-методы используются по смыслу, ошибки возвращаются честно, данные проходят валидацию, а важные проверки находятся на сервере. Это звучит как база, но именно база чаще всего и ломается.

Если делаешь первый API, не пытайся сразу построить огромную архитектуру. Начни с простого: один ресурс, несколько маршрутов, JSON-ответы, нормальные статус-коды и валидация. Потом подключишь базу, авторизацию, документацию и тесты.

На практике API — это скелет проекта. Frontend может быть красивым, дизайн может быть дорогим, кнопки могут плавно появляться с анимацией. Но если backend отвечает как попало, данные теряются, ошибки непонятны, а маршруты называются случайно — проект будет ощущаться сырым. API как раз и делает приложение не просто интерфейсом, а настоящим рабочим продуктом.

Последние статьи