Explorar o código

✨ feat(api): add openapi support with zod validation

- add @hono/zod-openapi package for openapi schema generation
- create api route structure with /api/v1/hello endpoint
- implement openapi documentation for hello endpoint

📦 build: update build scripts and dependencies

- add tsx package for improved development experience
- update dev script to use tsx instead of node
- modify server build path from src/entry-server.tsx to src/server/index.tsx
- rename entry-server.tsx to src/server/index.tsx for better organization

♻️ refactor(server): adjust import paths and server configuration

- update server.js to import and route API endpoints
- modify SSR render import path to new server index location
- update type import for render function in server.js
yourname hai 7 meses
pai
achega
e33d8a50cf
Modificáronse 6 ficheiros con 150 adicións e 12 borrados
  1. 4 2
      package.json
  2. 92 5
      pnpm-lock.yaml
  3. 11 4
      server.js
  4. 10 0
      src/server/api.ts
  5. 32 0
      src/server/api/hello/index.ts
  6. 1 1
      src/server/index.tsx

+ 4 - 2
package.json

@@ -4,14 +4,15 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "PORT=8080 node server",
+    "dev": "PORT=8080 tsx server",
     "build": "npm run build:client && npm run build:server",
     "build:client": "vite build --outDir dist/client",
-    "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
+    "build:server": "vite build --ssr src/server/index.tsx --outDir dist/server",
     "preview": "PORT=8080 cross-env NODE_ENV=production node server"
   },
   "dependencies": {
     "@hono/node-server": "^1.17.1",
+    "@hono/zod-openapi": "^1.0.2",
     "compression": "^1.8.0",
     "express": "^5.1.0",
     "hono": "^4.8.5",
@@ -26,6 +27,7 @@
     "@types/react-dom": "^19.1.6",
     "@vitejs/plugin-react-swc": "^3.10.2",
     "cross-env": "^7.0.3",
+    "tsx": "^4.20.3",
     "typescript": "~5.8.3",
     "vite": "^7.0.0"
   }

+ 92 - 5
pnpm-lock.yaml

@@ -11,6 +11,9 @@ importers:
       '@hono/node-server':
         specifier: ^1.17.1
         version: 1.17.1(hono@4.8.5)
+      '@hono/zod-openapi':
+        specifier: ^1.0.2
+        version: 1.0.2(hono@4.8.5)(zod@4.0.8)
       compression:
         specifier: ^1.8.0
         version: 1.8.1
@@ -44,19 +47,27 @@ importers:
         version: 19.1.6(@types/react@19.1.8)
       '@vitejs/plugin-react-swc':
         specifier: ^3.10.2
-        version: 3.11.0(vite@7.0.5(@types/node@24.1.0))
+        version: 3.11.0(vite@7.0.5(@types/node@24.1.0)(tsx@4.20.3)(yaml@2.8.0))
       cross-env:
         specifier: ^7.0.3
         version: 7.0.3
+      tsx:
+        specifier: ^4.20.3
+        version: 4.20.3
       typescript:
         specifier: ~5.8.3
         version: 5.8.3
       vite:
         specifier: ^7.0.0
-        version: 7.0.5(@types/node@24.1.0)
+        version: 7.0.5(@types/node@24.1.0)(tsx@4.20.3)(yaml@2.8.0)
 
 packages:
 
+  '@asteasolutions/zod-to-openapi@8.0.0':
+    resolution: {integrity: sha512-C56hBPiraeSWUNLz8mB5Z0/0LdfaFD5d6WB/+hdUg0MiC7egTgvWRGh3M3jZ3CRl03l/NJWnmv5D3OUAz+JGeg==}
+    peerDependencies:
+      zod: ^4.0.0
+
   '@esbuild/aix-ppc64@0.25.8':
     resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
     engines: {node: '>=18'}
@@ -219,6 +230,19 @@ packages:
     peerDependencies:
       hono: ^4
 
+  '@hono/zod-openapi@1.0.2':
+    resolution: {integrity: sha512-zbxUZEnA+NaeotXRI2YPL5GEbz38DiO7Zp1nx/7yXOA2ITkcASYsYe/My/I38c44GCu+oUVM899zn4TBVn7JRg==}
+    engines: {node: '>=16.0.0'}
+    peerDependencies:
+      hono: '>=4.3.6'
+      zod: ^4.0.0
+
+  '@hono/zod-validator@0.7.2':
+    resolution: {integrity: sha512-ub5eL/NeZ4eLZawu78JpW/J+dugDAYhwqUIdp9KYScI6PZECij4Hx4UsrthlEUutqDDhPwRI0MscUfNkvn/mqQ==}
+    peerDependencies:
+      hono: '>=3.9.0'
+      zod: ^3.25.0 || ^4.0.0
+
   '@polka/url@1.0.0-next.29':
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
 
@@ -616,6 +640,9 @@ packages:
     resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
     engines: {node: '>= 0.4'}
 
+  get-tsconfig@4.10.1:
+    resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
   gopd@1.2.0:
     resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
     engines: {node: '>= 0.4'}
@@ -711,6 +738,9 @@ packages:
   once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 
+  openapi3-ts@4.5.0:
+    resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==}
+
   parseurl@1.3.3:
     resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
     engines: {node: '>= 0.8'}
@@ -759,6 +789,9 @@ packages:
     resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
     engines: {node: '>=0.10.0'}
 
+  resolve-pkg-maps@1.0.0:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
   rollup@4.45.1:
     resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -840,6 +873,11 @@ packages:
     resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
     engines: {node: '>=6'}
 
+  tsx@4.20.3:
+    resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
   type-is@2.0.1:
     resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
     engines: {node: '>= 0.6'}
@@ -908,8 +946,21 @@ packages:
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
+  yaml@2.8.0:
+    resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
+    engines: {node: '>= 14.6'}
+    hasBin: true
+
+  zod@4.0.8:
+    resolution: {integrity: sha512-+MSh9cZU9r3QKlHqrgHMTSr3QwMGv4PLfR0M4N/sYWV5/x67HgXEhIGObdBkpnX8G78pTgWnIrBL2lZcNJOtfg==}
+
 snapshots:
 
+  '@asteasolutions/zod-to-openapi@8.0.0(zod@4.0.8)':
+    dependencies:
+      openapi3-ts: 4.5.0
+      zod: 4.0.8
+
   '@esbuild/aix-ppc64@0.25.8':
     optional: true
 
@@ -992,6 +1043,19 @@ snapshots:
     dependencies:
       hono: 4.8.5
 
+  '@hono/zod-openapi@1.0.2(hono@4.8.5)(zod@4.0.8)':
+    dependencies:
+      '@asteasolutions/zod-to-openapi': 8.0.0(zod@4.0.8)
+      '@hono/zod-validator': 0.7.2(hono@4.8.5)(zod@4.0.8)
+      hono: 4.8.5
+      openapi3-ts: 4.5.0
+      zod: 4.0.8
+
+  '@hono/zod-validator@0.7.2(hono@4.8.5)(zod@4.0.8)':
+    dependencies:
+      hono: 4.8.5
+      zod: 4.0.8
+
   '@polka/url@1.0.0-next.29': {}
 
   '@rolldown/pluginutils@1.0.0-beta.27': {}
@@ -1163,11 +1227,11 @@ snapshots:
       '@types/node': 24.1.0
       '@types/send': 0.17.5
 
-  '@vitejs/plugin-react-swc@3.11.0(vite@7.0.5(@types/node@24.1.0))':
+  '@vitejs/plugin-react-swc@3.11.0(vite@7.0.5(@types/node@24.1.0)(tsx@4.20.3)(yaml@2.8.0))':
     dependencies:
       '@rolldown/pluginutils': 1.0.0-beta.27
       '@swc/core': 1.13.2
-      vite: 7.0.5(@types/node@24.1.0)
+      vite: 7.0.5(@types/node@24.1.0)(tsx@4.20.3)(yaml@2.8.0)
     transitivePeerDependencies:
       - '@swc/helpers'
 
@@ -1375,6 +1439,10 @@ snapshots:
       dunder-proto: 1.0.1
       es-object-atoms: 1.1.1
 
+  get-tsconfig@4.10.1:
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+
   gopd@1.2.0: {}
 
   has-symbols@1.1.0: {}
@@ -1441,6 +1509,10 @@ snapshots:
     dependencies:
       wrappy: 1.0.2
 
+  openapi3-ts@4.5.0:
+    dependencies:
+      yaml: 2.8.0
+
   parseurl@1.3.3: {}
 
   path-key@3.1.1: {}
@@ -1482,6 +1554,8 @@ snapshots:
 
   react@19.1.0: {}
 
+  resolve-pkg-maps@1.0.0: {}
+
   rollup@4.45.1:
     dependencies:
       '@types/estree': 1.0.8
@@ -1606,6 +1680,13 @@ snapshots:
 
   totalist@3.0.1: {}
 
+  tsx@4.20.3:
+    dependencies:
+      esbuild: 0.25.8
+      get-tsconfig: 4.10.1
+    optionalDependencies:
+      fsevents: 2.3.3
+
   type-is@2.0.1:
     dependencies:
       content-type: 1.0.5
@@ -1620,7 +1701,7 @@ snapshots:
 
   vary@1.1.2: {}
 
-  vite@7.0.5(@types/node@24.1.0):
+  vite@7.0.5(@types/node@24.1.0)(tsx@4.20.3)(yaml@2.8.0):
     dependencies:
       esbuild: 0.25.8
       fdir: 6.4.6(picomatch@4.0.3)
@@ -1631,9 +1712,15 @@ snapshots:
     optionalDependencies:
       '@types/node': 24.1.0
       fsevents: 2.3.3
+      tsx: 4.20.3
+      yaml: 2.8.0
 
   which@2.0.2:
     dependencies:
       isexe: 2.0.0
 
   wrappy@1.0.2: {}
+
+  yaml@2.8.0: {}
+
+  zod@4.0.8: {}

+ 11 - 4
server.js

@@ -9,7 +9,13 @@ import { createServer as createNodeServer } from 'node:http';
 import process from 'node:process';
 
 // 创建 Hono 应用
-const app = new Hono();
+const app = new Hono();// API路由
+
+import api from './src/server/api.ts';
+
+
+app.route('/', api);
+
 
 // Constants
 const isProduction = process.env.NODE_ENV === 'production';
@@ -21,6 +27,7 @@ const ABORT_DELAY = 10000;
 const baseUrl = new URL(base, `http://localhost:${port}`);
 
 
+
 // 使用 Hono 的 serve 启动服务器
 const parentServer = serve({
   fetch: app.fetch,
@@ -127,14 +134,14 @@ app.use(async (c) => {
     // 处理所有其他请求的 SSR 逻辑
     /** @type {string} */
     let template;
-    /** @type {import('./src/entry-server.ts').render} */
+    /** @type {import('./src/server/index.tsx').render} */
     let render;
     
     if (!isProduction && vite) {
       // 开发环境:读取最新模板并转换
       template = await fs.readFile('./index.html', 'utf-8');
       template = await vite.transformIndexHtml(normalizedUrl, template);
-      render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render;
+      render = (await vite.ssrLoadModule('/src/server/index.tsx')).render;
     } else {
       // 生产环境:使用缓存的模板
       template = templateHtml;
@@ -206,4 +213,4 @@ app.use(async (c) => {
     console.error(e.stack);
     return c.text(e.stack, 500);
   }
-});
+});

+ 10 - 0
src/server/api.ts

@@ -0,0 +1,10 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import helloRoute from './api/hello/index';
+
+// 创建主API路由器
+const api = new OpenAPIHono();
+
+// 注册hello路由
+api.route('/api/v1/hello', helloRoute);
+
+export default api;

+ 32 - 0
src/server/api/hello/index.ts

@@ -0,0 +1,32 @@
+import { createRoute, OpenAPIHono, z} from '@hono/zod-openapi';
+
+// 响应Schema
+const HelloResponse = z.object({
+  message: z.string().openapi({
+    description: '返回的问候消息',
+    example: 'hello'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/',
+  responses: {
+    200: {
+      description: '成功返回问候消息',
+      content: {
+        'application/json': {
+          schema: HelloResponse
+        }
+      }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono().openapi(routeDef, async (c) => {
+  return c.json({ message: 'hello' }, 200);
+});
+
+export default app;

+ 1 - 1
src/entry-server.tsx → src/server/index.tsx

@@ -3,7 +3,7 @@ import {
   type RenderToPipeableStreamOptions,
   renderToPipeableStream,
 } from 'react-dom/server'
-import App from './App'
+import App from '../App'
 
 export function render(_url: string, options?: RenderToPipeableStreamOptions) {
   return renderToPipeableStream(