From 36d917eadd5bb0cd5d21a3e0ed7e3bbba1eba2c8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 03:05:02 +0000 Subject: [PATCH] Refactor Hono API routes for typed RPC client & lint fixes --- AGENTS.md | 14 ++++++++- src/api/asset.ts | 27 +++++++--------- src/api/character.ts | 62 ++++++++++++++++++++++--------------- src/api/equipment.ts | 6 ++-- src/api/extract.ts | 6 ++-- src/api/generation.ts | 20 +++++++----- src/service/asset.test.ts | 8 ++--- src/ui/components/Stash.tsx | 2 +- 8 files changed, 85 insertions(+), 60 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 63eca11..1905dae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,19 @@ We follow a strict layered architecture: 2. **Service Layer (`src/service`)**: Contains business logic and interacts with the Database via Prisma. 3. **Data Layer (`src/db.ts`)**: Exports the Prisma Client instance. +### Hono API Definitions + +- **Route Chaining**: Always define API routes using method chaining on a Hono instance. This ensures that the type definitions for inputs and outputs are correctly inferred and preserved in the exported type. +- **Export Pattern**: Export the chained route instance as the default export. + ```typescript + const app = new Hono() + const route = app + .get('/', ...) + .post('/', ...) + export default route + ``` +- **Type Safety**: Use `zValidator` for request validation (query, json, form) to ensure full end-to-end type safety with the Hono RPC client. + ## Frontend Architecture The frontend is built with: @@ -39,7 +52,6 @@ The frontend is built with: - **Run Dev**: `bun --hot src/index.ts` (Runs on port 3000) - ## Testing We use `bun:test` for testing. diff --git a/src/api/asset.ts b/src/api/asset.ts index 9e90c2e..c5f2e4d 100644 --- a/src/api/asset.ts +++ b/src/api/asset.ts @@ -1,25 +1,22 @@ import { Hono } from 'hono' +import { zValidator } from '@hono/zod-validator' +import { z } from 'zod' import { AssetService } from '../service/asset' -const asset = new Hono() +const app = new Hono() -asset.post('/upload', async (c) => { - const body = await c.req.parseBody() - const file = body['file'] - const name = body['name'] as string +const uploadSchema = z.object({ + file: z.instanceof(File), + name: z.string().min(1), +}) - if (!file || !(file instanceof File)) { - return c.json({ error: 'File is required' }, 400) - } - - if (!name) { - return c.json({ error: 'Name is required' }, 400) - } +const route = app.post('/upload', zValidator('form', uploadSchema), async (c) => { + const { file, name } = c.req.valid('form') try { - const result = await AssetService.createAsset(file, { + const result = await AssetService.createAsset(file as File, { name, - type: file.type, + type: (file as File).type, }) return c.json(result, 201) } catch (error) { @@ -28,4 +25,4 @@ asset.post('/upload', async (c) => { } }) -export default asset +export default route diff --git a/src/api/character.ts b/src/api/character.ts index b422378..2c5be85 100644 --- a/src/api/character.ts +++ b/src/api/character.ts @@ -1,36 +1,48 @@ import { Hono } from 'hono' +import { zValidator } from '@hono/zod-validator' +import { z } from 'zod' import { CharacterService } from '../service/character' -const character = new Hono() +const app = new Hono() -character.get('/', async (c) => { - const page = parseInt(c.req.query('page') || '1') - const limit = parseInt(c.req.query('limit') || '10') - const result = await CharacterService.listCharacters({ page, limit }) - return c.json(result) +const listSchema = z.object({ + page: z.coerce.number().optional().default(1), + limit: z.coerce.number().optional().default(10), }) -character.post('/', async (c) => { - const body = await c.req.json() - const { name, description, imageId } = body - if (!name || !imageId) { - return c.json({ error: 'Name and Image ID are required' }, 400) - } - const result = await CharacterService.createCharacter({ name, description, imageId }) - return c.json(result, 201) +const createSchema = z.object({ + name: z.string().min(1), + description: z.string().optional(), + imageId: z.string().min(1), }) -character.put('/:id', async (c) => { - const id = c.req.param('id') - const body = await c.req.json() - const result = await CharacterService.updateCharacter(id, body) - return c.json(result) +const updateSchema = z.object({ + name: z.string().optional(), + description: z.string().optional(), + imageId: z.string().optional(), }) -character.delete('/:id', async (c) => { - const id = c.req.param('id') - await CharacterService.deleteCharacter(id) - return c.json({ success: true }, 200) -}) +const route = app + .get('/', zValidator('query', listSchema), async (c) => { + const { page, limit } = c.req.valid('query') + const result = await CharacterService.listCharacters({ page, limit }) + return c.json(result) + }) + .post('/', zValidator('json', createSchema), async (c) => { + const { name, description, imageId } = c.req.valid('json') + const result = await CharacterService.createCharacter({ name, description, imageId }) + return c.json(result, 201) + }) + .put('/:id', zValidator('json', updateSchema), async (c) => { + const id = c.req.param('id') + const body = c.req.valid('json') + const result = await CharacterService.updateCharacter(id, body) + return c.json(result) + }) + .delete('/:id', async (c) => { + const id = c.req.param('id') + await CharacterService.deleteCharacter(id) + return c.json({ success: true }, 200) + }) -export default character +export default route diff --git a/src/api/equipment.ts b/src/api/equipment.ts index 2515f8e..b174604 100644 --- a/src/api/equipment.ts +++ b/src/api/equipment.ts @@ -3,7 +3,7 @@ import { zValidator } from '@hono/zod-validator' import { z } from 'zod' import { EquipmentService } from '../service/equipment' -const equipment = new Hono() +const app = new Hono() const listSchema = z.object({ page: z.coerce.number().optional().default(1), @@ -12,7 +12,7 @@ const listSchema = z.object({ subCategoryId: z.string().optional(), }) -equipment.get('/', zValidator('query', listSchema), async (c) => { +const route = app.get('/', zValidator('query', listSchema), async (c) => { const { page, limit, categoryId, subCategoryId } = c.req.valid('query') const result = await EquipmentService.listEquipments( @@ -23,4 +23,4 @@ equipment.get('/', zValidator('query', listSchema), async (c) => { return c.json(result) }) -export default equipment +export default route diff --git a/src/api/extract.ts b/src/api/extract.ts index 69b41e4..c21e7e8 100644 --- a/src/api/extract.ts +++ b/src/api/extract.ts @@ -8,13 +8,13 @@ import { EquipmentService } from '../service/equipment' import { CategoryService } from '../service/category' import { AssetService } from '../service/asset' -const extract = new Hono() +const app = new Hono() const extractSchema = z.object({ image: z.instanceof(File), }) -extract.post('/', zValidator('form', extractSchema), async (c) => { +const route = app.post('/', zValidator('form', extractSchema), async (c) => { const { image } = c.req.valid('form') const file = image as File @@ -138,4 +138,4 @@ extract.post('/', zValidator('form', extractSchema), async (c) => { }) }) -export default extract +export default route diff --git a/src/api/generation.ts b/src/api/generation.ts index 9e902eb..970aff7 100644 --- a/src/api/generation.ts +++ b/src/api/generation.ts @@ -1,13 +1,19 @@ import { Hono } from 'hono' +import { zValidator } from '@hono/zod-validator' +import { z } from 'zod' import { GenerationService } from '../service/generation' -const generation = new Hono() +const app = new Hono() -generation.get('/', async (c) => { - const page = parseInt(c.req.query('page') || '1') - const limit = parseInt(c.req.query('limit') || '10') - const characterId = c.req.query('characterId') - const equipmentId = c.req.query('equipmentId') +const listSchema = z.object({ + page: z.coerce.number().optional().default(1), + limit: z.coerce.number().optional().default(10), + characterId: z.string().optional(), + equipmentId: z.string().optional(), +}) + +const route = app.get('/', zValidator('query', listSchema), async (c) => { + const { page, limit, characterId, equipmentId } = c.req.valid('query') const result = await GenerationService.listGenerations( { page, limit }, @@ -16,4 +22,4 @@ generation.get('/', async (c) => { return c.json(result) }) -export default generation +export default route diff --git a/src/service/asset.test.ts b/src/service/asset.test.ts index 6eee0cc..29f2ab9 100644 --- a/src/service/asset.test.ts +++ b/src/service/asset.test.ts @@ -7,15 +7,13 @@ const mockPrisma = { }, } -const mockFileSystem = { - writeFile: mock(), -} - mock.module('../db', () => ({ prisma: mockPrisma, })) -const mockBunWrite = mock((path: string, content: any) => Promise.resolve(content.length)) +const mockBunWrite = mock((_path: string, content: unknown) => + Promise.resolve((content as Blob | string).toString().length), +) // @ts-ignore Bun.write = mockBunWrite diff --git a/src/ui/components/Stash.tsx b/src/ui/components/Stash.tsx index e7ee5b0..9ed677a 100644 --- a/src/ui/components/Stash.tsx +++ b/src/ui/components/Stash.tsx @@ -26,7 +26,7 @@ export const Stash: React.FC = ({ onNavigateToExtractor }) => { }, }) if (res.ok) { - const data = (await res.json()) as any + const data = await res.json() setItems(data.items) } } catch (e) {