Blog (CRUD)

A full create-read-update-delete blog built on the Hono backend and the typed RPC client

The blog feature is a complete CRUD example built on the decoupled architecture: the backend (apps/api) owns the posts table and exposes REST endpoints, and the frontend (apps/web) manages posts through the typed Hono RPC client. There is no database access in the frontend.

Endpoints

All endpoints require a session. Users can only read, update, and delete their own posts.

MethodPathDescription
GET/api/postsList the current user's posts (newest first)
GET/api/posts/:idGet a single post
POST/api/postsCreate a post
PATCH/api/posts/:idUpdate a post
DELETE/api/posts/:idDelete a post

The routes live in apps/api/src/features/blog/routes.ts and are mounted into the Hono app through the managed routes marker in apps/api/src/app.ts.

Schema

The posts table is contributed by the feature into packages/db/src/schema.ts (via the managed tables marker):

ColumnTypeNotes
idtextPrimary key (UUID)
titletextRequired
contenttextDefaults to ""
publishedbooleanDefaults to false (draft)
authorIdtextReferences user.id
createdAt / updatedAttimestampManaged automatically

Frontend Pages

apps/web/app/(dashboard)/dashboard/posts/Directory Structure
apps/web/app/(dashboard)/dashboard/posts/
├── page.tsx              # List + delete
├── new/page.tsx          # Create
└── [id]/edit/page.tsx    # Update

A single reusable form powers create and update:

Code
apps/web/components/blog/post-form.tsx

Calling the API (typed RPC)

tsx
"use client"import { api } from "@/lib/api-client"
// Createawait api.api.posts.$post({ json: { title, content, published } })
// Listconst res = await api.api.posts.$get()const posts = await res.json()
// Updateawait api.api.posts[":id"].$patch({ param: { id }, json: { title } })
// Deleteawait api.api.posts[":id"].$delete({ param: { id } })

Install / Uninstall

This feature is managed by the feature manager:

bash
pnpm add-feature blog       # copies files, applies insertions, adds drizzle-orm to apps/apipnpm --filter @workspace/db db:push   # create the posts tablepnpm remove-feature blog    # reverses all of the above

Then open http://localhost:3000/dashboard/posts.