CRUD API статей
CRUD — четыре базовых действия с данными: создать (Create), прочитать (Read), изменить (Update), удалить (Delete). Соберём их для статей блога в одном файле, используя всё из прошлых уроков: группу маршрутов, zod, HTTPException и базу SQLite.
Маршруты
| Запрос | Действие | Код ответа |
|---|---|---|
GET /posts | список статей | 200 |
GET /posts/:id | одна статья | 200 или 404 |
POST /posts | создание | 201 |
PUT /posts/:id | изменение | 200 или 404 |
DELETE /posts/:id | удаление | 204 или 404 |
Файл маршрутов
Замените содержимое src/routes/posts.ts:
// src/routes/posts.ts
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { db, type Post } from '../db.js'
import { postSchema } from '../schemas.js'
const idParam = z.object({ id: z.coerce.number().int().positive() })
function findPost(id: number): Post {
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id) as Post | undefined
if (!post) {
throw new HTTPException(404, { message: 'Статья не найдена' })
}
return post
}
const posts = new Hono()
posts.get('/', (c) => {
const rows = db.prepare('SELECT * FROM posts ORDER BY id DESC').all() as Post[]
return c.json(rows)
})
posts.get('/:id', zValidator('param', idParam), (c) => {
const { id } = c.req.valid('param')
return c.json(findPost(id))
})
posts.post('/', zValidator('json', postSchema), (c) => {
const { title, body } = c.req.valid('json')
const result = db
.prepare('INSERT INTO posts (title, body) VALUES (?, ?)')
.run(title, body)
return c.json(findPost(Number(result.lastInsertRowid)), 201)
})
posts.put('/:id', zValidator('param', idParam), zValidator('json', postSchema), (c) => {
const { id } = c.req.valid('param')
const { title, body } = c.req.valid('json')
findPost(id)
db.prepare('UPDATE posts SET title = ?, body = ? WHERE id = ?').run(title, body, id)
return c.json(findPost(id))
})
posts.delete('/:id', zValidator('param', idParam), (c) => {
const { id } = c.req.valid('param')
findPost(id)
db.prepare('DELETE FROM posts WHERE id = ?').run(id)
return c.body(null, 204)
})
export default postsФункция findPost() ищет статью и выбрасывает HTTPException, если её нет. Благодаря ей проверка на 404 написана один раз, а app.onError() из урока об ошибках превратит исключение в JSON. В маршрут put подключены два валидатора: для номера в адресе и для тела.
Проверка
Создайте статью:
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title": "Hono и SQLite", "body": "Статья из базы"}'В ответ придёт статья с номером, датой и кодом 201. Дальше:
curl http://localhost:3000/posts/1
curl -X PUT http://localhost:3000/posts/1 \
-H "Content-Type: application/json" \
-d '{"title": "Новый заголовок", "body": "Новый текст"}'
curl -i -X DELETE http://localhost:3000/posts/1Первая команда вернёт статью, вторая — её обновлённую версию, третья — пустой ответ с кодом 204. После удаления GET /posts/1 вернёт 404. Данные сохраняются и после перезапуска сервера.
Комментарии
Маршруты комментариев в src/routes/comments.ts строятся так же: GET /posts/:postId/comments выбирает строки WHERE post_id = ?, а POST вставляет комментарий с номером статьи из адреса. Перед вставкой проверьте, что статья существует, — иначе получится комментарий к несуществующей статье.
Частая ошибка
Ответ 204 отправляют через c.json(null, 204) или с телом-сообщением. Код 204 означает «нет содержимого», и тело у такого ответа должно быть пустым. Используйте c.body(null, 204), а если хотите вернуть сообщение — выберите код 200.