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 ofexpress.Router(), you create anew 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-validatorfor 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
.prismafile. 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 generateand 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
- Prisma — What is Prisma?
- Getting started with PostgreSQL
- Prisma Schema reference
- CRUD operations
- Prisma Migrate
- Prisma Studio
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 Verb | Path | Description |
|---|---|---|
| GET | /api/items | List all wishlist items |
| POST | /api/items | Create a new item |
| GET | /api/items/:id | Get a specific item |
| PUT | /api/items/:id | Update a specific item |
| DELETE | /api/items/:id | Delete a specific item |
Steps to get started:
- Scaffold a new Hono project:
bun create hono@latest(select thenodejstemplate). - Install Prisma:
bun add @prisma/clientandbun add -D prisma. - Initialize Prisma:
bunx prisma init— this creates aprisma/schema.prismafile and a.envwith aDATABASE_URLplaceholder. - Define your models in
prisma/schema.prismaand runbunx prisma migrate devto create and apply migrations. - Create your route handlers. Use
app.route('/api/items', itemRoutes)to keep things organized. - Validate request bodies with
@hono/zod-validatorand Zod schemas.
Exercise 2 — Enhancing the API
After the basic CRUD operations are implemented, let's add more features:
- Filtering — As a user, I want to be able to filter wishlist items by name. Add support for a
searchquery parameter on the list endpoint (e.g.,GET /api/items?search=book). Use Prisma'scontainsfilter withmode: 'insensitive'for case-insensitive matching. - Pagination — Add
pageandlimitquery parameters to the list endpoint. Validate them with Zod (usingz.coerce.number()since query params are strings). - Error handling — Return proper error responses (404 when an item doesn't exist, 422 for validation errors). Use Hono's
HTTPExceptionandapp.onErrorfor centralized error handling.