Зарегистрируйтесь, чтобы продолжить обучение

Реализация CRUD JS: REST API (Fastify)

Теперь, когда проект настроен, база данных подключена, и вы получили общее представление о современной 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/:id.

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/:id. В данном случае :id это плейсхолдер, который будет заменен на конкретное число во время формирования ответа.

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/:id

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, и закладывают основу для более сложных операций и взаимодействий. В дальнейшем эти базовые операции могут быть расширены и дополнены в зависимости от требований вашего приложения, например, добавлением авторизации, валидации данных, обработки ошибок и других аспектов, характерных для реального продакшена.


Самостоятельная работа

  1. Добавьте набор операций CRUD для каждой сущности: пользователь, курс, урок
  2. Запушьте изменения в репозиторий

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff