Skip to content

快速开始 ​

本文带你创建一个 hazejs 项目并运行起来,然后亲手添加第一个路由和控制器。

版本约定

@hazejs/* 各包与 create-hazejs 共用一个版本号,遵循语义化版本。升级时请一起升级,并查看更新日志了解变化。

环境要求 ​

  • Node.js 22.12 及以上(Vite 8 的要求)。
  • 推荐使用 pnpm,npm 和 yarn 同样可用。

创建项目 ​

sh
pnpm create hazejs my-app
sh
npm create hazejs@latest my-app
sh
yarn create hazejs my-app

不带 -t 参数时会交互式地让你选择模板,也可以直接指定:

sh
pnpm create hazejs my-app -t api     # 纯后端 API
pnpm create hazejs my-app -t vue     # Vue 3 全栈
pnpm create hazejs my-app -t react   # React 19 全栈

TIP

使用 npm 时,参数需要用 -- 与包名隔开:npm create hazejs@latest my-app -- -t vue。

模板内容
api纯后端服务,代码直接位于项目根目录(app/、config/、route/)
vueVue 3 + hazejs:同一个仓库、同一个端口、一次构建,后端代码位于 server/
react同样的全栈结构,前端换成 React 19
module可发布的模块包(路由、中间件、配置与迁移),供其它项目在 config/modules.ts 中启用

在非交互环境中(CI、没有 TTY)默认使用 api 模板。目标目录非空时需要加 --force(会先清空该目录);加 --auto-import 可开启自动导入,控制器、服务与框架 API 无需手写 import。

脚手架会:

  • 复制模板,并根据目录名设置 package.json 的 name;
  • 在 .env 中写入一个随机生成的 APP_KEY,用于签名 Cookie、加密 Session,同时也是 JWT 的默认密钥;
  • 生成 pnpm-workspace.yaml,声明依赖安装脚本的处理方式(pnpm 11 要求显式声明);如果项目创建在已有的 pnpm workspace 内则跳过。

脚手架不会自动安装依赖,接下来手动安装:

sh
cd my-app
pnpm install

模板里有什么 ​

api 模板是一个完整可运行的示例,而不是空壳,包括:

  • 带请求校验的 GET /api/hello 与 POST /api/hello;
  • 基于 SQLite 和 User 模型的 users 资源接口,输出由 API 资源(app/resource/UserResource.ts)控制,邮箱只对本人和管理员可见;
  • JWT 注册、登录和 /api/auth/me,以及“只能修改自己资料”的授权策略;
  • /api/auth 下的账号接口:个人访问令牌(/tokens)、邮箱验证、找回密码,以及 GitHub 登录(在 .env 中填写 GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET 后启用,配置在 config/oauth.ts,使用 @hazejs/oauth);
  • 流式 AI 对话接口 POST /api/chat(config/ai.ts);
  • 事件、监听器、队列任务、邮件和定时任务各一个示例;
  • 一个通知(WelcomeNotification,经邮件和广播发送),以及每个用户的私有广播频道(app/channel/user.ts);
  • lang/en 与 lang/zh-CN 两套翻译和 config/i18n.ts,欢迎邮件、校验错误与框架消息都按请求的语言输出(见多语言);
  • 一个示例命令 haze inspire(app/command/Inspire.ts);
  • 位于 /api/ws 的 WebSocket 路由;
  • 覆盖以上功能的测试。

全栈模板在此基础上增加了 src/ 下的 Vue / React 前端(含一个 AI 对话组件)和一个 Socket.IO 命名空间。每个目录的用途见目录结构。

运行项目 ​

模板默认使用 SQLite(data/app.db)。首次运行前先执行一次迁移建表,再启动开发服务器:

sh
pnpm db:migrate
pnpm dev

api 模板监听 http://localhost:3000:

sh
curl localhost:3000/api/hello
# {"code":0,"message":"ok","data":{"app":"my-app","message":"Hello, hazejs!"}}

全栈项目则由 Vite 启动,使用 Vite 的端口(默认 http://localhost:5173),页面和 /api/** 共用这一个端口,详见 Vite 全栈。

还有几个页面无需任何配置即可使用:开发环境下,/__haze 是开发面板(请求、查询、任务、邮件、日志),/docs 是根据路由生成的 OpenAPI 文档;/health 在任何模式下都会响应健康检查。全栈项目中 Vite 开发服务器只把 /api/** 与 /__haze 交给后端,需要后两者时,把 OpenAPI 的地址(config/openapi.ts 的 path 与 docsPath)和健康检查的地址(config/health.ts 的 path)移到 /api 下。

修改任意控制器、服务、路由或配置文件,下一个请求就会用上新代码。进程不会重启,终端只会打印一行 reloaded。

第一个路由和控制器 ​

先生成一个服务和一个控制器:

sh
npx haze make:service Post      # app/service/PostService.ts
npx haze make:controller Post   # app/controller/PostController.ts

服务负责业务逻辑,不感知 HTTP:

ts
// app/service/PostService.ts
export class PostService {
  private posts = [
    { id: 1, title: 'Hello hazejs' },
    { id: 2, title: 'Second post' },
  ]

  all() {
    return this.posts
  }

  find(id: number) {
    return this.posts.find(post => post.id === id)
  }
}

控制器接收请求,通过 inject() 调用服务,直接返回数据即可:

ts
// app/controller/PostController.ts
import type { HttpContext } from '@hazejs/core'
import { inject, NotFoundException } from '@hazejs/core'
import { PostService } from '~/app/service/PostService'

export class PostController {
  private posts = inject(PostService)

  index() {
    return this.posts.all()
  }

  show(ctx: HttpContext) {
    const post = this.posts.find(Number(ctx.param('id')))
    if (!post)
      throw new NotFoundException(`Post ${ctx.param('id')} not found`)
    return post
  }
}

~ 是指向后端源码目录的别名,无论文件放在哪一层,导入路径都保持一致。路由不会从控制器自动推导,需要在 route/api.ts 已有的 /api 分组里注册:

ts
// route/api.ts
import { PostController } from '~/app/controller/PostController'

export default defineRoutes((r) => {
  r.group({ prefix: '/api', as: 'api.' }, (r) => {
    // ...已有的路由
    r.get('/posts', [PostController, 'index']).name('posts.index')
    r.get('/posts/:id', [PostController, 'show']).name('posts.show')
  })
})

保存后试一下:

sh
curl localhost:3000/api/posts/1
# {"code":0,"message":"ok","data":{"id":1,"title":"Hello hazejs"}}

curl localhost:3000/api/posts/9
# 404,由异常处理器按同样的响应结构输出

运行 pnpm routes 可以在路由表中看到新增的两条。由于路由带了名字,生成类型之后(开发服务器会自动生成),router.url('api.posts.show', { id: 1 }) 的路由名和参数都会经过类型检查。

加一个测试 ​

sh
npx haze make:test post   # tests/post.test.ts
ts
// tests/post.test.ts
import { useTestApp } from '@hazejs/testing'
import { describe, expect, it } from 'vitest'

const t = useTestApp()

describe('posts', () => {
  it('shows a post', async () => {
    const res = await t.get('/api/posts/1')
    expect(res.status).toBe(200)
    expect(res.body.data).toEqual({ id: 1, title: 'Hello hazejs' })
  })
})
sh
pnpm test

测试在进程内启动真实的应用,不监听端口。详见测试。

默认脚本 ​

所有模板都自带以下脚本:

脚本命令作用
devhaze dev开发服务器,原地热替换
buildhaze build构建生产产物(dist/)
starthaze start运行生产产物
previewhaze preview按构建时的部署预设在本地运行产物
queuehaze queue:work消费队列任务
schedulehaze schedule:work常驻执行定时任务(每分钟检查)
testhaze test运行 Vitest
routeshaze route:list打印路由表
typechecktsc --noEmit类型检查(全栈模板为 vue-tsc --build / tsc --build)
typegenhaze typegen生成配置键、路由、授权能力与翻译键的类型,以及类型安全客户端使用的路由表
db:generatehaze db:generate根据表结构变化生成迁移
db:migratehaze db:migrate执行尚未运行的迁移
db:seedhaze db:seed执行数据填充

额外的参数会原样传给命令,例如 pnpm db:generate --name create_posts。所有命令的说明见命令行参考。

AI 编码助手 ​

hazejs 提供了 Agent Skills,让 Claude Code、Cursor、Codex 等支持 skill 的编码助手掌握框架的约定与 API。用 skills 命令行安装到项目中:

sh
npx skills add penna-org/haze

命令会列出可用的 skill,并询问要安装哪些、装给哪些助手:

Skill用途
hazejs用 hazejs 开发应用:路由、校验、数据库、认证、队列、实时通信、AI、测试、部署等
hazejs-migration把 Fastify、Express、Koa、Hono 或 NestJS 后端迁移到 hazejs,同时保持原有接口契约
hazejs-contributing开发框架本身(penna-org/haze 仓库)

也可以直接指定:--skill hazejs --agent claude-code;加 -g 安装到用户级而不是当前项目;升级 hazejs 后运行 npx skills update 更新。配合开发者工具的 MCP 端点,助手还能直接查看正在运行的应用。

下一步 ​

基于 MIT 许可证发布