Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

CRUD API для статей

CRUD — четыре базовых действия с данными: создать (Create), прочитать (Read), изменить (Update), удалить (Delete). Здесь все они собираются в один файл маршрутов статей, который работает с базой из прошлого урока и проверяет данные схемами.

Маршруты API

Метод и адресДействиеКод успеха
GET /postsСписок статей200
GET /posts/:idОдна статья200
POST /postsСоздать статью201
PUT /posts/:idИзменить статью200
DELETE /posts/:idУдалить статью204

Схемы

Схемы вынесены в начало файла src/routes/posts.js, чтобы маршруты оставались короткими:

// src/routes/posts.js
const params = {
  type: 'object',
  properties: { id: { type: 'integer' } },
};
 
const postInput = {
  type: 'object',
  required: ['title', 'body'],
  properties: {
    title: { type: 'string', minLength: 3, maxLength: 120 },
    body: { type: 'string', minLength: 1 },
  },
};

params переводит id в число, postInput проверяет тело при создании и изменении.

Маршруты

Ниже в том же файле:

// src/routes/posts.js
export default async function postRoutes(app) {
  const findPost = (id) => {
    const post = app.db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
    if (!post) {
      throw app.httpErrors.notFound('Статья не найдена');
    }
    return post;
  };
 
  app.get('/posts', async () => {
    return app.db.prepare('SELECT id, title, created_at FROM posts ORDER BY id DESC').all();
  });
 
  app.get('/posts/:id', { schema: { params } }, async (request) => {
    return findPost(request.params.id);
  });
 
  app.post('/posts', { schema: { body: postInput } }, async (request, reply) => {
    const { title, body } = request.body;
    const result = app.db
      .prepare('INSERT INTO posts (title, body) VALUES (?, ?)')
      .run(title, body);
    return reply.code(201).send(findPost(result.lastInsertRowid));
  });
 
  app.put('/posts/:id', { schema: { params, body: postInput } }, async (request) => {
    const { id } = request.params;
    findPost(id);
    const { title, body } = request.body;
    app.db.prepare('UPDATE posts SET title = ?, body = ? WHERE id = ?').run(title, body, id);
    return findPost(id);
  });
 
  app.delete('/posts/:id', { schema: { params } }, async (request, reply) => {
    const result = app.db.prepare('DELETE FROM posts WHERE id = ?').run(request.params.id);
    if (result.changes === 0) {
      throw app.httpErrors.notFound('Статья не найдена');
    }
    return reply.code(204).send();
  });
}

Функция findPost используется в четырёх маршрутах: она либо возвращает статью, либо бросает ошибку 404.

Проверка

curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Привет, Fastify","body":"Первая статья в базе"}'
 
curl http://localhost:3000/posts
curl -X DELETE -i http://localhost:3000/posts/1

Первая команда вернёт статью с id и created_at, вторая — список, третья — ответ 204 No Content. После перезапуска сервера статьи остаются на месте.

Комментарии

Маршруты комментариев в src/routes/comments.js строятся так же. Например, добавление комментария к статье:

// src/routes/comments.js
app.post('/posts/:id/comments', async (request, reply) => {
  const { author, text } = request.body;
  const result = app.db
    .prepare('INSERT INTO comments (post_id, author, text) VALUES (?, ?, ?)')
    .run(request.params.id, author, text);
  return reply.code(201).send({ id: result.lastInsertRowid, author, text });
});

Добавьте к нему схемы params и body по образцу статей.

Частая ошибка

Маршрут PUT без проверки существования статьи «успешно» обновит ноль строк и вернёт пустой ответ с кодом 200. Сначала ищите запись и бросайте 404, если её нет, — как это делает findPost.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.