JS: REST API (Fastify)

Теория: Реализация CRUD

Теперь, когда проект настроен, база данных подключена, и вы получили общее представление о современной REST-архитектуре, настало время перейти к практике. В этом уроке мы сосредоточимся на создании простых API, чтобы закрепить теоретические знания на конкретных примерах. Рассмотрим работу с ресурсом User и реализуем CRUD-операции (Create, Read, Update, Delete) для него.

Список пользователей

Начнем с реализации эндпоинта для получения списка всех пользователей. Этот эндпоинт будет располагаться по адресу /api/users. Для этого создадим файл routes/api/users.js со следующим содержимым:

import { asc } from 'drizzle-orm'
import * as schemas from '../../db/schema.js'

export default async function (fastify) {
  const db = fastify.db

  fastify.get(
    '/users',
    async function (request) {
      const users = await db.query
        .users
        .findMany({
          orderBy: asc(schemas.users.id),
        })

      return users
    },
  )
}

Разберем код подробнее:

  • Fastify автоматически добавляет префикс /api в адрес, так как это файл находится внутри директории routes/api.
  • Fastify автоматически преобразует возвращаемый объект в JSON и отдает его клиенту с выставленными заголовками. По умолчанию код ответа 200.

Проверим, что все работает:

  1. Запустим сервер в отдельной вкладке

    npm run dev
    > fastify start -w -l info -P app.js
    
    [21:01:55.683] INFO (47440): Server listening at http://127.0.0.1:3000
    [21:01:55.685] INFO (47440): Server listening at http://[::1]:3000
  2. Выполним запрос на /api/users с помощью curl. Запрос вернет записи, описанные в файле с сидами. Каждый перезапуск сервера будет возвращать разные данные, так как внутри используется faker для их генерации.

    # node-jq форматирует вывод для более красивого представления
    curl localhost:3000/api/users | npx node-jq
    [
      {
        "id": 1,
        "fullName": "Miss Olivia Johns",
        "email": "Jeffery_Harris@yahoo.com",
        "updatedAt": null,
        "createdAt": "1724980072"
      },
      {
        "id": 2,
        "fullName": "Lance Beer",
        "email": "Lew_Hoeger@hotmail.com",
        "updatedAt": null,
        "createdAt": "1724980072"
      }
    ]

В реальных приложениях часто требуется ограничить количество возвращаемых записей, например, при выводе длинных списков. Для этого используется постраничная навигация, которую можно реализовать с помощью параметров LIMIT и OFFSET в SQL-запросе.

fastify.get(
  '/users',
  async function (request) {
    // Количество элементов на страницу
    const perPage = 1
    // Текущая страница со значением по умолчанию
    const { page = 1 } = request.query
    const users = await db.query
      .users
      .findMany({
        orderBy: asc(schemas.users.id),
        limit: perPage, // количество записей на страницу
        offset: (page - 1) * perPage, // текущая страница
      })

    return users
  },
)

LIMIT ограничивает количество записей, возвращаемых запросом, а OFFSET определяет, с какой записи начинать выборку. Таким образом, для получения первой страницы используется offset = 0, для второй — offset = 1, и так далее.

Параметр page позволяет клиенту указать, какую страницу данных он хочет получить. Например, запрос с page=2 вернет вторую страницу данных. Теперь, если вы выполните запрос с параметром page, вы получите соответствующую страницу пользователей:

curl "localhost:3000/api/users?page=2" | npx node-jq
[
  {
    "id": 2,
    "fullName": "Lance Beer",
    "email": "Lew_Hoeger@hotmail.com",
    "updatedAt": null,
    "createdAt": "1724980072"
  }
]

Один пользователь

Следующим шагом добавим возможность получения данных конкретного пользователя по его идентификатору (id). Эндпоинт будет располагаться по адресу /api/users/

.

fastify.get(
  '/users/:id',
  async (request) => {
    const user = await db.query.users.findFirst({
      where: eq(schemas.users.id, request.params.id),
    })
    fastify.assert(user, 404)
    return user
  },
)

Этот эндпоинт принимает параметр id из URL и использует его для поиска пользователя в базе данных. Если пользователь с таким идентификатором найден, он возвращается в виде JSON-объекта. В случае, если пользователь не найден, возвращается ошибка с кодом 404.

Последнее поведение реализовано с помощью механизма fastify.assert(). Он позволяет сократить логику проверки существования объекта и сразу вернуть правильный код ответа если объекта не существует. Первым параметром метод принимает объект, который может быть null, вторым - код ответа если вместо объекта был передан null.

Создание пользователя

Для создания нового пользователя добавим следующий API-эндпоинт /api/users. В отличие от предыдущих, он выполняется методом POST.

fastify.post(
  '/users',
  async (request, reply) => {
    const [user] = await db.insert(schemas.users)
      .values(request.body)
      .returning()

    return reply.code(201)
      .send(user)
  },
)

Этот эндпоинт принимает данные нового пользователя в теле запроса и сохраняет их в базе данных. После успешного создания пользователя сервер возвращает код 201 и данные нового пользователя. Из-за изменения кода ответа, нам недостаточно просто вернуть объект с данными. Здесь мы возвращаем объект reply установив внутри него код ответа и добавив данные через метод send(), который сам преобразует данные в JSON.

Обновление пользователя

Для обновления данных существующего пользователя добавим API-эндпоинт /api/users/

. В данном случае
это плейсхолдер, который будет заменен на конкретное число во время формирования ответа.

fastify.patch(
  '/users/:id',
  async (request) => {
    const [user] = await db.update(schemas.users)
      .set(request.body)
      .where(eq(schemas.users.id, request.params.id))
      .returning()
    fastify.assert(user, 404)

    return user
  },
)

Этот эндпоинт позволяет обновить данные пользователя с указанным id. Запрос должен содержать новые данные в теле. Если пользователь с таким id найден и обновлен, возвращается обновленный объект пользователя. Если пользователя не существует, возвращается ошибка с кодом 404.

Удаление пользователя

Для удаления пользователя по его идентификатору добавим следующий API-эндпоинт /api/users/

fastify.delete(
  '/users/:id',
  async (request, reply) => {
    const [user] = await db.delete(schemas.users)
      .where(eq(schemas.users.id, request.params.id))
      .returning()
    fastify.assert(user, 404)
    // Обязательно вызывать send(), иначе обработка зависнет
    return reply.code(204).send()
  },
)

Этот эндпоинт удаляет пользователя с указанным id из базы данных. Если пользователь найден и успешно удален, возвращается код 204 без тела ответа. Если пользователь не найден, возвращается ошибка с кодом 404. Такое поведение DELETE соответствует спецификации HTTP.

Заключение

С этими эндпоинтами мы реализовали полный набор операций CRUD (Create, Read, Update, Delete) для работы с ресурсом пользователя в нашем API. Эти примеры охватывают основные операции, которые вы будете использовать при разработке REST API, и закладывают основу для более сложных операций и взаимодействий. В дальнейшем эти базовые операции могут быть расширены и дополнены в зависимости от требований вашего приложения, например, добавлением авторизации, валидации данных, обработки ошибок и других аспектов, характерных для реального продакшена.

Рекомендуемые программы

Завершено

0 / 15