REST API

When an application starts to grow, it's common practice to split the frontend (client part) from the backend (server part). If the application is big enough, this usually helps with maintainability.

In this section we are going to create a REST API to manage our wishlist items. We will use Hono as our web framework and Prisma to interact with the database.

Bun

Throughout this guide we will use Bun as our JavaScript runtime and package manager instead of Node.js + npm. Bun is a fast, all-in-one toolkit that includes a runtime, bundler, test runner, and package manager. It is fully compatible with Node.js APIs and npm packages, but significantly faster at installing dependencies and running scripts.

Install it with:

curl -fsSL https://bun.sh/install | bash

After installation, verify it works:

bun --version

From here on, every command in this guide uses bun (instead of npm) and bunx (instead of npx).

Resources

Hono

Hono (Japanese for "flame") is an ultrafast, lightweight web framework built on Web Standards (the Request/Response API). It has first-class TypeScript support, zero dependencies, and runs on virtually any JavaScript runtime: Node.js, Deno, Bun, Cloudflare Workers, and more.

If you've used Express before, Hono will feel familiar but more modern. The key differences are:

  • Built-in JSON parsing — no need for a body-parser middleware, just call await c.req.json().
  • Sub-apps via app.route() — instead of express.Router(), you create a new Hono() and mount it on a path.
  • Context object (c) — all request/response operations go through the context: c.req.param(), c.req.query(), c.json(), etc.
  • Validation — built-in integration with Zod via @hono/zod-validator for end-to-end type-safe request validation.

Quick example:

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ id })
})

app.post('/users', async (c) => {
  const body = await c.req.json()
  return c.json({ created: body }, 201)
})

serve({ fetch: app.fetch, port: 3000 })

Resources

Prisma

An ORM (Object-Relational Mapping) is a library that lets you interact with a database using your programming language instead of writing raw SQL. If you're unfamiliar with the concept, watch this Introduction to ORMs video first.

Prisma is a next-generation TypeScript ORM that takes a schema-first approach. Instead of defining your database schema in TypeScript code, you declare your models in a dedicated schema.prisma file, and Prisma generates a fully type-safe client tailored to your schema. This means you get autocompletion for every model, field, and relation — and type errors at compile time if your queries don't match your database.

Key characteristics:

  • Prisma Schema Language (PSL) — you define your models, fields, and relations in a .prisma file. This serves as the single source of truth for your database structure and the generated client.
  • Prisma Client — an auto-generated, type-safe query builder. After any schema change, run prisma generate and your client is updated with the new types. Queries use an intuitive, object-based API (prisma.user.findMany(), prisma.item.create(), etc.).
  • Prisma Migrate — a migration tool that generates SQL migration files from schema changes, tracks migration history, and applies them to your database.
  • Prisma Studio — a visual database browser that lets you view and edit your data directly in the browser.

Quick example:

// schema.prisma — define your models
model Item {
  id        Int      @id @default(autoincrement())
  name      String   @db.VarChar(255)
  purchased Boolean  @default(false)
  createdAt DateTime @default(now()) @map("created_at")
}
// usage — query with the Prisma Client
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// SELECT * FROM items WHERE purchased = false
const wishlist = await prisma.item.findMany({ where: { purchased: false } })

// INSERT INTO items (name) VALUES ('New Item') RETURNING *
const newItem = await prisma.item.create({ data: { name: 'New Item' } })

// UPDATE items SET purchased = true WHERE id = 1
await prisma.item.update({ where: { id: 1 }, data: { purchased: true } })

// DELETE FROM items WHERE id = 1
await prisma.item.delete({ where: { id: 1 } })

Resources

Exercise 1 — Building the API

Using Hono and Prisma, create REST endpoints under the /api namespace to manage wishlist items. Your API should support the standard CRUD operations:

HTTP VerbPathDescription
GET/api/itemsList all wishlist items
POST/api/itemsCreate a new item
GET/api/items/:idGet a specific item
PUT/api/items/:idUpdate a specific item
DELETE/api/items/:idDelete a specific item

Steps to get started:

  1. Scaffold a new Hono project: bun create hono@latest (select the nodejs template).
  2. Install Prisma: bun add @prisma/client and bun add -D prisma.
  3. Initialize Prisma: bunx prisma init — this creates a prisma/schema.prisma file and a .env with a DATABASE_URL placeholder.
  4. Define your models in prisma/schema.prisma and run bunx prisma migrate dev to create and apply migrations.
  5. Create your route handlers. Use app.route('/api/items', itemRoutes) to keep things organized.
  6. Validate request bodies with @hono/zod-validator and Zod schemas.

Exercise 2 — Enhancing the API

After the basic CRUD operations are implemented, let's add more features:

  1. Filtering — As a user, I want to be able to filter wishlist items by name. Add support for a search query parameter on the list endpoint (e.g., GET /api/items?search=book). Use Prisma's contains filter with mode: 'insensitive' for case-insensitive matching.
  2. Pagination — Add page and limit query parameters to the list endpoint. Validate them with Zod (using z.coerce.number() since query params are strings).
  3. Error handling — Return proper error responses (404 when an item doesn't exist, 422 for validation errors). Use Hono's HTTPException and app.onError for centralized error handling.