Вивчай

API Routes

Next.js API Routes — кухня ресторану з вікном видачі де кухар передає страву офіціантуNext.js API Routes — кухня ресторану з вікном видачі де кухар передає страву офіціанту

У попередніх уроках ми навчилися створювати сторінки та отримувати дані. Але що, якщо нам потрібен свій сервер для обробки форм, збереження даних або інтеграції з іншими сервісами? Next.js дозволяє створювати API endpoints прямо в проекті -- без окремого бекенду!


Навіщо API Routes?

У звичайному React-проекті для серверної логіки потрібен окремий сервер (Express, Fastify тощо). Next.js вирішує це вбудованими API routes:

  • Обробка форм (контактна форма, реєстрація)
  • CRUD операції з базою даних
  • Інтеграція з зовнішніми API (приховування ключів)
  • Webhook-обробники
  • Авторизація
Інфо

API routes виконуються тільки на сервері. Код у них ніколи не потрапляє в бандл браузера. Тут безпечно використовувати секретні ключі, підключення до БД тощо.


Route Handlers (App Router)

В App Router API-ендпоінти створюються через файл route.ts (не page.tsx!):

src/app/
  api/
    hello/
      route.ts            → GET /api/hello
    users/
      route.ts            → GET/POST /api/users
      [id]/
        route.ts          → GET/PUT/DELETE /api/users/:id

Перший API route

// src/app/api/hello/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({
    message: "Привіт з API!",
    timestamp: new Date().toISOString(),
  });
}

Тепер відкрий http://localhost:3000/api/hello -- побачиш JSON-відповідь!


HTTP-методи

Кожен метод -- це окрема експортована функція:

// src/app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

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

// Імітація бази даних (в реальному проекті -- БД)
let users: User[] = [
  { id: 1, name: "Олексій", email: "alex@example.com" },
  { id: 2, name: "Марія", email: "maria@example.com" },
];

// GET /api/users -- отримати всіх
export async function GET() {
  return NextResponse.json(users);
}

// POST /api/users -- створити нового
export async function POST(request: NextRequest) {
  const body = await request.json();

  const newUser: User = {
    id: users.length + 1,
    name: body.name,
    email: body.email,
  };

  users.push(newUser);

  return NextResponse.json(newUser, { status: 201 });
}
Увага

Масив users тут зберігається в пам'яті -- при перезапуску сервера дані зникнуть. У реальному проекті використовуй базу даних (Firebase, PostgreSQL, MongoDB тощо).


Динамічні API routes

Для операцій з конкретним ресурсом (GET одного, PUT, DELETE):

// src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";

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

// Імітація БД (у реальному проекті -- спільна БД)
let users: User[] = [
  { id: 1, name: "Олексій", email: "alex@example.com" },
  { id: 2, name: "Марія", email: "maria@example.com" },
];

// GET /api/users/1 -- отримати одного
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const user = users.find((u) => u.id === Number(id));

  if (!user) {
    return NextResponse.json(
      { error: "Користувача не знайдено" },
      { status: 404 }
    );
  }

  return NextResponse.json(user);
}

// PUT /api/users/1 -- оновити
export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const body = await request.json();
  const index = users.findIndex((u) => u.id === Number(id));

  if (index === -1) {
    return NextResponse.json(
      { error: "Користувача не знайдено" },
      { status: 404 }
    );
  }

  users[index] = { ...users[index], ...body };
  return NextResponse.json(users[index]);
}

// DELETE /api/users/1 -- видалити
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const index = users.findIndex((u) => u.id === Number(id));

  if (index === -1) {
    return NextResponse.json(
      { error: "Користувача не знайдено" },
      { status: 404 }
    );
  }

  const deleted = users.splice(index, 1)[0];
  return NextResponse.json({ message: "Видалено", user: deleted });
}

Робота з Request

NextRequest надає зручні методи для роботи із запитом:

Query параметри

// GET /api/users?search=олексій&page=2
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const search = searchParams.get("search"); // "олексій"
  const page = Number(searchParams.get("page")) || 1; // 2

  // Фільтрація та пагінація
  let result = users;

  if (search) {
    result = result.filter((u) =>
      u.name.toLowerCase().includes(search.toLowerCase())
    );
  }

  const perPage = 10;
  const start = (page - 1) * perPage;
  result = result.slice(start, start + perPage);

  return NextResponse.json({
    data: result,
    page,
    total: users.length,
  });
}

Headers та Cookies

export async function GET(request: NextRequest) {
  // Читання headers
  const authHeader = request.headers.get("authorization");
  const contentType = request.headers.get("content-type");

  // Читання cookies
  const token = request.cookies.get("session")?.value;

  // Встановлення headers у відповіді
  const response = NextResponse.json({ data: "test" });
  response.headers.set("X-Custom-Header", "hello");

  // Встановлення cookies
  response.cookies.set("visited", "true", {
    httpOnly: true,
    maxAge: 60 * 60 * 24, // 1 день
  });

  return response;
}

Валідація запитів

Завжди перевіряй вхідні дані:

// src/app/api/users/route.ts
interface CreateUserBody {
  name: string;
  email: string;
}

function validateUser(body: unknown): body is CreateUserBody {
  if (!body || typeof body !== "object") return false;
  const obj = body as Record<string, unknown>;
  if (typeof obj.name !== "string" || obj.name.length < 2) return false;
  if (typeof obj.email !== "string" || !obj.email.includes("@")) return false;
  return true;
}

export async function POST(request: NextRequest) {
  let body: unknown;

  try {
    body = await request.json();
  } catch {
    return NextResponse.json(
      { error: "Невалідний JSON" },
      { status: 400 }
    );
  }

  if (!validateUser(body)) {
    return NextResponse.json(
      { error: "Невалідні дані. Потрібно: name (мін. 2), email (з @)" },
      { status: 400 }
    );
  }

  // Тепер body типізований як CreateUserBody
  const newUser = {
    id: Date.now(),
    name: body.name,
    email: body.email,
  };

  return NextResponse.json(newUser, { status: 201 });
}
Порада

Для серйозних проектів використовуй бібліотеки валідації: Zod, Yup, або Valibot. Вони дають і TypeScript типи, і runtime-валідацію в одному місці.


Виклик API з клієнта

Як використовувати створені API routes з React-компонентів:

// src/components/UserList.tsx
"use client";

import { useState, useEffect } from "react";

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

export default function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  // Завантаження списку
  useEffect(() => {
    fetch("/api/users")
      .then((res) => res.json())
      .then(setUsers);
  }, []);

  // Створення нового
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const res = await fetch("/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, email }),
    });

    if (res.ok) {
      const newUser = await res.json();
      setUsers((prev) => [...prev, newUser]);
      setName("");
      setEmail("");
    }
  };

  // Видалення
  const handleDelete = async (id: number) => {
    const res = await fetch(`/api/users/${id}`, { method: "DELETE" });
    if (res.ok) {
      setUsers((prev) => prev.filter((u) => u.id !== id));
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Ім'я" />
        <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
        <button type="submit">Додати</button>
      </form>

      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} ({user.email})
            <button onClick={() => handleDelete(user.id)}>Видалити</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Підсумок

  • Route Handlers (route.ts) створюють API endpoints в Next.js
  • Експортуй функції GET, POST, PUT, DELETE для відповідних HTTP-методів
  • NextRequest -- для читання body, query params, headers, cookies
  • NextResponse.json() -- для створення JSON-відповідей зі статус-кодами
  • Завжди валідуй вхідні дані на сервері
  • API routes виконуються тільки на сервері -- безпечно для секретів

Що далі?

У наступному уроці зануримося в App Router -- розберемо layouts, Server Components vs Client Components, та сучасну архітектуру Next.js додатків.

Інфо

Корисні посилання: