Merge pull request #4 from Yiling-J/refactor-hono-api-routes-1758627388033681198
Refactor Hono API routes for typed RPC client & lint fixes
This commit is contained in:
8 files changed
+85
-60
No files matched your search
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user