Making a Hono API using Claude Code - the Wrong Way
One of my favorite quotes from literature is the opening line of Tolstoy’s Anna Karenina:
Happy families are all alike; every unhappy family is unhappy in its own way.
Of course, it isn’t quite true, but from Tolstoy’s perspective it was way more interesting to write and read about a family gone wrong than to write and read about one that is running smoothly, with no problems at all.
In software, fortunately there are many ways of doing things right (I will write about one of those ways in my next post). But there are many ways of doing things wrong, too. One of those ways is to jump too far onto the Gen AI bandwagon, having AI handle not only coding but judgement as well. Gergely Orosz of The Pragmatic Engineer has a great comment about the value of expertise when writing code with AI Agents:
The more I have AI agents write all my code, the more I feel that us devs will be alright (and possibly more in-demand for important stuff)
Hard for me to imagine anyone building reliable software without an understanding of how to do this (either via experience or study)
— Gergely Orosz (@gergely.pragmaticengineer.com) January 22, 2026 at 4:49 AM
What I’m going to write is not going to be reliable. In fact it’s not going to work at all without a bit of prodding.
The Project: a Bicycle Shop API
I wanted to simulate something close to a real world example with a relational data model and a mixture of non-functional requirements, so I chose a bicycle shop that maintains a record of their customers and those customers’ orders. Here is the prompt:
You are a bike store owner. You are writing a web service for tracking customers and orders. You need to create a sample database that will be the basis of the production database. This API is production-ready. Also generate a typescript client SDK for this web service.
Claude wanted to ask clarifying questions, but that seemed to be against the point of the test. From the perspective of a software engineer, these are pretty good questions, though:
Tool use rejected with user message: The user has indicated they have provided enough answers for the plan interview.
Stop asking clarifying questions and proceed to finish the plan with the information you have.
Questions asked and answers provided:
- "Which framework and database would you like to use for the web service?"
(No answer provided)
- "Which runtime should the server use?"
(No answer provided)
For each of these questions it had given me options:
A choice of framework
- Hono (JS framework) + Sqlite
- Express + PostgreSQL
- Hono + PostgreSQL
A choice of runtime
- Node.js
- Deno
- Bun
Claude Code, given no preferences, chose the last option each time.
The Result
Claude took 5 minutes to come up with a plan and create the web service with the test database. Here is how it described its work.
What was created
Bun workspaces monorepo with two packages:
packages/api — Hono + PostgreSQL + Drizzle ORM
┌─────────────────────────┬───────────────────────────────────────────────────────────────────────────────────┐
│ File │ Purpose │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/db/schema.ts │ 4 tables: customers, products, orders, order_items with enums, indexes, relations │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/db/client.ts │ Drizzle client initialized from DATABASE_URL │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/db/migrate.ts │ Migration runner │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/db/seed.ts │ 12 customers, 25 products (bikes/accessories/apparel/parts), 18 orders │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/routes/customers.ts │ Full CRUD + search/pagination + customer order history │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/routes/products.ts │ Full CRUD + filters (category, brand, in-stock) + soft delete │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/routes/orders.ts │ Create with stock check & transaction, status state machine, line items │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ src/middleware/error.ts │ Centralized error handler for HTTP, Zod, and unexpected errors │
├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤
│ drizzle.config.ts │ Drizzle Kit config for migrations │
└─────────────────────────┴───────────────────────────────────────────────────────────────────────────────────┘
packages/sdk — TypeScript client (zero dependencies)
import { BikeStoreClient } from '@bike-store/sdk'
const client = new BikeStoreClient({ baseUrl: 'http://localhost:3000' })
const { data: customers } = await client.customers.list({ search: 'portland' })
const order = await client.orders.create({ customerId: '...', items: [...] })
await client.orders.updateStatus(order.data.id, 'confirmed')
To get started
cp .env.example .env # set DATABASE_URL
bun install
bun run db:generate # generate migrations
bun run db:migrate # apply to PostgreSQL
bun run db:seed # load sample data
bun run dev # start on :3000
Critically, I couldn’t actually run those steps, as Claude did not ask to install Bun or PostgreSQL, and did not attempt to run either during this process. When I tried to run these scripts in order, I found that bun was not installed. I then received the error DATABASE_URL is required once I did install it and attempt to run bun run db:generate. I told Claude what happened.
I'm getting the message "DATABASE_URL is required"
Claude tried updating the scripts in the package.json to fix the problem, but still had not tried to run any of the scripts that it had written. I followed up by asking Claude to fix it on its own.
try fixing the problem on your own
Claude took the following steps:
- Checked if Homebrew was installed on my development machine (a MacBook)
- Installed PostgreSQL using Homebrew (I approved this)
- Started PostgreSQL locally using Homebrew
- Created the database on the local PostgreSQL instance
- Installed Bun
- Fixed the way environment variables were being inherited in the Drizzle config
- Ran migrations to fill the database with data
- Started the service using Bun and confirmed that it was up and running.
I ran a very basic call using curl to confirm that the customers route was working.
curl http://localhost:3000/customers
It returned a JSON list of the customers. I removed the email addresses because I cannot confirm that they are not real.
{
"data": [
{
"id": "16ab5a8a-2d8c-4afe-98f3-4b5f23ee85cb",
"firstName": "Marcus",
"lastName": "Benson",
"email": "",
"phone": "555-142-8831",
"address": "678 Aspen Way",
"city": "Boulder",
"state": "CO",
"zipCode": "80301",
"loyaltyPoints": 4100,
"createdAt": "2026-03-04T13:49:05.672Z",
"updatedAt": "2026-03-04T13:49:05.672Z"
},
{
"id": "12fc033a-719d-4782-b2c0-91aa99997368",
"firstName": "Linda",
"lastName": "Chung",
"email": "",
"phone": "555-623-8864",
"address": "990 Willow Court",
"city": "San Francisco",
"state": "CA",
"zipCode": "94102",
"loyaltyPoints": 2100,
"createdAt": "2026-03-04T13:49:05.672Z",
"updatedAt": "2026-03-04T13:49:05.672Z"
},
// ... 10 more
],
"meta": {
"total": 12,
"page": 1,
"limit": 20,
"totalPages": 1
}
}
You can view the project yourself in the ApiTest repository on my Github account.
What’s wrong?
Well, for starters I asked for a production-ready web service and this is quite simply not production-ready.
is this fully ready for production deployment?
Honest answer: not quite. It's a solid foundation, but here are the gaps that matter for real production:
Critical gaps
No authentication — The X-API-Key header is accepted but never validated. Any request gets full access.
No rate limiting — Nothing prevents abuse or accidental DoS.
CORS is wide open — .env defaults to *, which is fine for dev but not production.
No graceful shutdown — The server doesn't drain in-flight requests before stopping, which causes dropped
connections during deploys.
Health check doesn't test the DB — /health returns ok even if Postgres is down.
No environment validation at startup — Missing env vars surface as runtime errors mid-request, not at boot.
Operational gaps
No Dockerfile / containerization — Can't deploy to any cloud platform without this.
No structured logging — Hono's logger writes plain text. Production needs JSON logs with request IDs for
aggregation.
No tests — Zero coverage on routes, validators, or the order state machine.
updatedAt is set manually — If you ever update via raw SQL or a migration, it won't be touched.
Minor but worth noting
- No API versioning (/v1/) — makes breaking changes painful later
- SDK has no retry logic or timeout configuration
- Products soft-delete but customers don't — inconsistent
- bun-types diagnostic warning in the workspace tsconfig
Claude Code hasn’t even pointed out that it did not add validation to the endpoints, meaning what should be HTTP Status 400 validation errors are instead HTTP Status 500 server errors.
So without guidance, I’ve used Claude Code to make a web service. With naïve guidance, I’ve gotten it to run on my computer. But it’s not close to production-ready, and Claude Code doesn’t even know all of the reasons why.
What did I learn?
You can’t just tell Claude Code to “make a production ready software product” and expect it to make it without guidance. Ultimately, Claude Code is a tool for software engineering, rather than a replacement for a software engineer. Claude Code knows how to write code quite well, but does not know how to write reliable and deliverable code without guidance — the application of judgement earned by software engineers from education and especially experience. It works better when you start from software engineering best practices, particularly writing clear documentation.
Anthropic has published Best Practices for using Claude Code. AI Agents are incredibly advanced tools, but the fundamental truth for tools still holds: you need to read the Claude Code Best Practices documentation.
In my next post, I will show an example of the same API written with Anthropic’s best practices in mind, showing how guidance can get the result you want out of agentic coding.