Refactor Hono API routes for typed RPC client & lint fixes

This commit is contained in:
google-labs-jules[bot] committed 2026-02-08 03:05:02 +00:00
1 parent 51252249eb
commit 36d917eadd
8 files changed
+85 -60

No files matched your search

+13 -1
View File
@@ -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.
+12 -15
View File
@@ -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
+37 -25
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+13 -7
View File
@@ -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
+3 -5
View File
@@ -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
+1 -1
View File
@@ -26,7 +26,7 @@ export const Stash: React.FC<StashProps> = ({ onNavigateToExtractor }) => {
},
})
if (res.ok) {
const data = (await res.json()) as any
const data = await res.json()
setItems(data.items)
}
} catch (e) {