Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A CRUD API for posts

CRUD is the four basic actions on data: Create, Read, Update, Delete. Let us build them for blog posts in one file, using everything from the previous lessons: a route group, zod, HTTPException and the SQLite database.

Routes

RequestActionStatus code
GET /postslist of posts200
GET /posts/:idone post200 or 404
POST /postscreate201
PUT /posts/:idupdate200 or 404
DELETE /posts/:iddelete204 or 404

The routes file

Replace the contents of 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: 'Post not found' })
  }
  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

The findPost() function looks up a post and throws an HTTPException if it does not exist. Thanks to it the 404 check is written once, and app.onError() from the error lesson turns the exception into JSON. The put route has two validators: one for the number in the address and one for the body.

Checking

Create a post:

curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "Hono and SQLite", "body": "A post from the database"}'

The response is the post with its number, date and code 201. Next:

curl http://localhost:3000/posts/1
curl -X PUT http://localhost:3000/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "New title", "body": "New text"}'
curl -i -X DELETE http://localhost:3000/posts/1

The first command returns the post, the second its updated version, the third an empty response with code 204. After deletion GET /posts/1 returns 404. The data survives a server restart.

Comments

The comment routes in src/routes/comments.ts are built the same way: GET /posts/:postId/comments selects rows WHERE post_id = ?, and POST inserts a comment with the post number from the address. Before inserting, check that the post exists — otherwise you get a comment on a post that does not exist.

Common mistake

Sending a 204 response with c.json(null, 204) or with a message body. Code 204 means "no content", and such a response must have an empty body. Use c.body(null, 204), and if you want to return a message, choose code 200.

Thanks for reading!

Donate to the site — we work hard for you.