资讯详情

jira_clone API 架构解析:基于 Node/TypeScript 与 TypeORM 的后端目录结构深度拆解

📅 2026/10/14 20:31:05 | 华诺云谱 👁 阅读
jira_clone API 架构解析:基于 Node/TypeScript 与 TypeORM 的后端目录结构深度拆解
前端后端企业应用【免费下载链接】jira_cloneA simplified Jira clone built with React/Babel (Client), and Node/TypeScript (API). Auto formatted with Prettier, tested with Cypress.项目地址https://gitcode.com/gh_mirrors/ji/jira_clone点击查看免费下载本篇技术文章以 jira_clone 仓库中 API 侧的官方结构文档 api/README.md 为主体逐目录拆解api/src下 constants、controllers、database、entities、errors、middleware、serializers、utils 等模块的职责与真实实现。读完后你不仅能理解这套Express TypeORM JWT后端的分层设计还能掌握路由隔离、catchErrors错误收敛、实体校验与序列化输出等可直接复用的工程实践。一、整体定位一个刻意保持简单的后端官方文档对 API 代码库的定性是一句话The API codebase is fairly simple and should be easy enough to understand.API 代码库相当简单应该不难理解。这种简单是有设计前提的它服务于一套简化版 Jira项目 Project、工作项 Issue、评论 Comment、用户 User 四个核心实体技术选型为 Node.js TypeScript Express TypeORMPostgreSQL并依赖jsonwebtoken做 Bearer Token 鉴权。从 api/package.json 的依赖清单可以印证这一点核心运行时依赖只有express、typeorm、pg、jsonwebtoken、cors、dotenv、lodash、striptags等少数几个包没有 ORM 之外的重型框架目录职责因此清晰、边界稳定。文档给出的完整目录说明表如下路径统一转换为以仓库根目录为起点文件或目录说明官方文档原文意译api/src/index.ts入口文件。在此设置中间件、挂载路由、初始化数据库连接和 Express 应用。api/src/routes.ts所有路由的定义处同时包含公开路由和私有路由。api/src/constants存放代码库多处用到的、不会改变的值常量。api/src/controllers控制器监听客户端请求操作实体和数据库完成数据的增删改查。api/src/database数据库相关代码和种子数据seeds。api/src/entitiesTypeORM 实体的定义处可以理解为模型。在此为每个数据库实体定义列、关联和校验。api/src/errors自定义错误定义。catchErrors函数帮助我们避免在控制器中写重复的try/catch块。api/src/middleware中间件函数可以修改请求和响应对象、结束请求-响应循环等。例如authenticateUser方法校验授权 token并把currentUser挂到请求对象上。api/src/serializers序列化器在数据从数据库取出之后、发送给客户端之前对其进行转换。api/src/utils代码库多处使用的工具helper函数。例如utils/typeorm.ts中的函数帮助我们校验数据、避免编写重复代码。下面逐目录展开用真实源码印证每个目录在请求生命周期中扮演的角色。二、入口文件启动顺序与中间件装配入口文件 api/src/index.ts 是理解整个架构最快的切入点。它按严格的顺序完成四件事import module-alias/register; import dotenv/config; import reflect-metadata; import express from express; import cors from cors; import createDatabaseConnection from database/createConnection; import { addRespondToResponse } from middleware/response; import { authenticateUser } from middleware/authentication; import { handleError } from middleware/errors; import { RouteNotFoundError } from errors; import { attachPublicRoutes, attachPrivateRoutes } from ./routes; const initializeExpress (): void { const app express(); app.use(cors()); app.use(express.json()); app.use(express.urlencoded()); app.use(addRespondToResponse); attachPublicRoutes(app); app.use(/, authenticateUser); attachPrivateRoutes(app); app.use((req, _res, next) next(new RouteNotFoundError(req.originalUrl))); app.use(handleError); app.listen(process.env.PORT || 3000); }; const initializeApp async (): Promisevoid { await establishDatabaseConnection(); initializeExpress(); }; initializeApp();几个值得注意的设计点顶部三行 import 的顺序不可调换module-alias/register负责运行时路径别名配合 api/tsconfig-paths.jsdotenv/config加载.envreflect-metadata是 TypeORM 装饰器的运行时前提。公开/私有路由的分界线是app.use(/, authenticateUser)所有公开路由先挂载鉴权中间件之后才挂私有路由之后未匹配的路径会抛RouteNotFoundError404最终由handleError统一兜底。数据库连接失败不阻止进程启动establishDatabaseConnection中捕获连接异常后仅console.log进程仍会继续进入initializeExpress()。路径别名为什么 import 能写database/createConnectionapi/tsconfig.json 中配置了baseUrl: src与paths: { *: [./*] }这使得源码中所有 import 都以src为基准书写如import { Issue } from entities既省去冗长的相对路径也让目录职责在 import 语句里一目了然。生产构建时则通过start:production脚本加载tsconfig-paths.js注册别名见下文运行章节。三、路由层公开路由与私有路由的物理隔离api/src/routes.ts 按认证要求把路由拆成两个挂载函数export const attachPublicRoutes (app: any): void { if (process.env.NODE_ENV test) { app.delete(/test/reset-database, test.resetDatabase); app.post(/test/create-account, test.createAccount); } app.post(/authentication/guest, authentication.createGuestAccount); }; export const attachPrivateRoutes (app: any): void { app.post(/comments, comments.create); app.put(/comments/:commentId, comments.update); app.delete(/comments/:commentId, comments.remove); app.get(/issues, issues.getProjectIssues); app.get(/issues/:issueId, issues.getIssueWithUsersAndComments); app.post(/issues, issues.create); app.put(/issues/:issueId, issues.update); app.delete(/issues/:issueId, issues.remove); app.get(/project, projects.getProjectWithUsersAndIssues); app.put(/project, projects.update); app.get(/currentUser, users.getCurrentUser); };从源码结构看这套路由设计有三层意图环境隔离的测试路由/test/reset-database和/test/create-account仅在NODE_ENV test时挂载专供 Cypress 端到端测试使用对应 api/src/controllers/test.ts避免测试专用的破坏性接口泄漏到生产环境。唯一的公开业务路由是POST /authentication/guest访客guest通过它获取初始账号和 token这解释了为什么前端打开页面即可无登录使用本应用。所有业务写操作都在鉴权线之后Issue、Comment、Project 的 CRUD 与GET /currentUser全部依赖req.currentUser。鉴权线背后的类型扩展req.currentUser并非 Express 原生属性而是由 api/src/types/express.d.ts 声明扩展declare namespace Express { export interface Response { respond: (data: any) void; } export interface Request { currentUser: import(entities).User; } }同一份声明里还扩展了res.respond——这正是下一节要讲的响应中间件挂载的自定义方法TypeScript 层面让挂扩展属性变得类型安全。四、控制器控制器只做编排不做重复劳动文档将 controllers 描述为监听客户端请求与实体和数据库交互完成数据的获取、添加、更新或删除。以最核心的 api/src/controllers/issues.ts 为例export const getProjectIssues catchErrors(async (req, res) { const { projectId } req.currentUser; const { searchTerm } req.query; let whereSQL issue.projectId :projectId; if (searchTerm) { whereSQL AND (issue.title ILIKE :searchTerm OR issue.descriptionText ILIKE :searchTerm); } const issues await Issue.createQueryBuilder(issue) .select() .where(whereSQL, { projectId, searchTerm: %${searchTerm}% }) .getMany(); res.respond({ issues }); }); export const create catchErrors(async (req, res) { const listPosition await calculateListPosition(req.body); const issue await createEntity(Issue, { ...req.body, listPosition }); res.respond({ issue }); });这段代码印证了文档对控制器编排者角色的定义并展示了两个典型的 Jira 业务细节看板搜索GET /issues用ILIKE对title和descriptionText去标签后的纯文本做模糊搜索。注意它复用的是实体上由钩子自动生成的descriptionText列而非直接搜 HTML 富文本这避免了标签噪音。列表位置算法新建 Issue 时calculateListPosition取同项目、同状态下所有listPosition的最小值减 1空列表则取 1即总是插到该列顶部。由于listPosition是double precision见 api/src/entities/Issue.ts列内插入无需重排整列。其余控制器同样薄comments.ts 的 create/update/remove 各只有 4 行users.ts 直接返回req.currentUserprojects.ts 读取当前用户所属项目时通过relations: [users, issues]一次性联查并用序列化器裁剪 issue 字段。五、errorscatchErrors如何消灭重复的 try/catch文档明确点出catchErrors的目的是avoid repetitive try/catch blocks within controllers。其全部实现在 api/src/errors/asyncCatch.tsexport const catchErrors (requestHandler: RequestHandler): RequestHandler { return async (req, res, next): Promiseany { try { return await requestHandler(req, res, next); } catch (error) { next(error); } }; };这是一个标准的 Express 高阶包装器任何被catchErrors包裹的异步处理器抛出异常后都会交给next(error)进入错误处理管道而无需手写 try/catch。所有路由处理器均被它包裹可全局检索catchErrors(async (req, res)验证。与之配套的是 api/src/errors/customErrors.ts 中定义的错误体系全部继承自带code、status、data三件套的CustomError错误类codestatus触发场景RouteNotFoundErrorROUTE_NOT_FOUND404入口文件末端的兜底路由EntityNotFoundErrorENTITY_NOT_FOUND404findEntityOrThrow查不到记录BadUserInputErrorBAD_USER_INPUT400实体校验失败data携带字段级错误InvalidTokenErrorINVALID_TOKEN401token 缺失/无效/对应用户不存在统一出口是 api/src/middleware/errors.ts只有CustomError实例的message/code/status/data会被原样返回给客户端其他任何未知异常一律折叠为INTERNAL_ERROR500防止内部细节泄漏export const handleError: ErrorRequestHandler (error, _req, res, _next) { console.error(error); const isErrorSafeForClient error instanceof CustomError; const clientError isErrorSafeForClient ? pick(error, [message, code, status, data]) : { message: Something went wrong, please contact our support., code: INTERNAL_ERROR, status: 500, data: {}, }; res.status(clientError.status).send({ error: clientError }); };这套自定义错误白名单 全局错误中间件的组合是小型 Express 服务做统一错误协议时的典型范式。六、middleware鉴权与响应约定authenticateUser三步验证链api/src/middleware/authentication.ts 实现了文档中描述的校验授权 token并把currentUser挂到请求对象上export const authenticateUser catchErrors(async (req, _res, next) { const token getAuthTokenFromRequest(req); if (!token) { throw new InvalidTokenError(Authentication token not found.); } const userId verifyToken(token).sub; if (!userId) { throw new InvalidTokenError(Authentication token is invalid.); } const user await User.findOne(userId); if (!user) { throw new InvalidTokenError(Authentication token is invalid: User not found.); } req.currentUser user; next(); });token 从Authorization请求头的Bearer token形式中解析JWT 验证逻辑在 api/src/utils/authToken.ts签名使用JWT_SECRET环境变量默认有效期 180 天。注意这里每请求都会User.findOne一次——从源码结构看这是一种以数据库为准的强一致校验即使 token 未过期用户被删除后请求也会立即失效。res.respond统一响应形状api/src/middleware/response.ts 只有 5 行export const addRespondToResponse: RequestHandler (_req, res, next) { res.respond (data): void { res.status(200).send(data); }; next(); };它为res挂载respond方法让所有成功响应固定为 HTTP 200 { ...业务数据 }的形状控制器因此不必关心状态码细节错误状态码则完全由错误中间件接管。七、entitiesTypeORM 实体与实体自带校验文档把 entities 解释为可以理解为模型并强调在这里定义列、关联、校验。四个实体为 User、Project、Issue、Comment由 api/src/entities/index.ts 统一导出并注入数据库连接entities: Object.values(entities)。以User为例展示实体自带校验这一项目特色Entity() class User extends BaseEntity { static validations { name: [is.required(), is.maxLength(100)], email: [is.required(), is.email(), is.maxLength(200)], }; PrimaryGeneratedColumn() id: number; Column(varchar) name: string; // ... createdAt / updatedAt 自动时间戳 ManyToMany(() Issue, issue issue.users) issues: Issue[]; ManyToOne(() Project, project project.users) project: Project; RelationId((user: User) user.project) projectId: number; }关键关系与业务语义User.projectId每个用户属于唯一项目projectId通过RelationId落库为普通整数列。所有私有路由都以req.currentUser.projectId作为数据隔离边界——这解释了为什么项目接口路径是GET /project而非GET /project/:id。Issue.usersManyToMany与userIdsIssue 有唯一reporterId但可有多名 assignee多对多关系用JoinTable()隐式中间表表达RelationId把关联用户 id 拍平成userIds: number[]便于序列化输出。Comment.issue上的onDelete: CASCADE删除 Issue 时评论随之级联删除无需手动清理。生命周期钩子Issue.ts 用BeforeInsert()BeforeUpdate()在保存前用striptags把富文本description剥成纯文本descriptionText专供全文检索与搜索使用。每个实体的static validations与 api/src/utils/validation.ts 中手写的轻量校验器is.required、is.maxLength、is.oneOf、is.email、is.url等配合把校验规则集中声明在实体上。八、utils把查询-校验-保存压缩成四个函数文档称 utils 存放代码库多处使用的工具函数其中 api/src/utils/typeorm.ts 是控制器能保持极薄的关键。它提供四个泛型函数export const findEntityOrThrow async T extends EntityConstructor( Constructor: T, id: number | string, options?: FindOneOptions, ): PromiseInstanceTypeT { const instance await Constructor.findOne(id, options); if (!instance) { throw new EntityNotFoundError(Constructor.name); } return instance; }; export const validateAndSaveEntity async T extends EntityInstance(instance: T): PromiseT { const Constructor entities[instance.constructor.name]; if (validations in Constructor) { const errorFields generateErrors(instance, Constructor.validations); if (Object.keys(errorFields).length 0) { throw new BadUserInputError({ fields: errorFields }); } } return instance.save() as PromiseT; }; export const createEntity async (Constructor, input) { const instance Constructor.create(input); return validateAndSaveEntity(instance); }; export const updateEntity async (Constructor, id, input) { const instance await findEntityOrThrow(Constructor, id); Object.assign(instance, input); return validateAndSaveEntity(instance); }; export const deleteEntity async (Constructor, id) { const instance await findEntityOrThrow(Constructor, id); await instance.remove(); return instance; };调用链为createEntity/updateEntity→validateAndSaveEntity跑static validations失败抛BadUserInputError并携带{ fields: ... }字段级错误→save()。查询侧则由findEntityOrThrow统一处理不存在即 404。这正对应文档所说的validate data and avoid writing repetitive code。utils 目录另有两个工具authToken.ts前文已述与 validation.ts校验器工厂generateErrors遍历字段校验器、每字段只保留第一条错误信息。九、serializers发送前的字段裁剪文档描述 serializers 的职责是在数据发给客户端前做转换。当前仓库中唯一的序列化器 api/src/serializers/issues.tsexport const issuePartial (issue: Issue): PartialIssue pick(issue, [ id, title, type, status, priority, listPosition, createdAt, updatedAt, userIds, ]);它把 Issue 精简为看板卡片所需的字段子集。使用点在 api/src/controllers/projects.tsGET /project返回的项目携带完整 issue 列表但每条 issue 都经过issuePartial裁剪——富文本description、估算工时等细节字段不会随项目接口下发客户端点开单个 issue 时再通过GET /issues/:issueId拉取全量含users、comments关联。这是一种典型的列表轻、详情重的负载控制策略。十、constants 与 database取值域约束与种子数据constants枚举即业务协议api/src/constants/issues.ts 与 api/src/constants/projects.ts 定义了全部枚举export enum IssueType { TASK task, BUG bug, STORY story, } export enum IssueStatus { BACKLOG backlog, SELECTED selected, INPROGRESS inprogress, DONE done, } export enum IssuePriority { HIGHEST 5, HIGH 4, MEDIUM 3, LOW 2, LOWEST 1, }这些枚举并非摆设实体的validations里is.oneOf(Object.values(IssueType))等规则直接引用它们意味着非法的 type/status/priority 值会在入库前被 400 拦下。值得注意IssuePriority用字符串1~5编码优先级数值越大优先级越高——这使前端可直接按值排序。database连接、种子与测试重置api/src/database/createConnection.ts 建立 PostgreSQL 连接配置项全部来自环境变量并开启synchronize: true按实体定义自动同步表结构const createDatabaseConnection (): PromiseConnection createConnection({ type: postgres, host: process.env.DB_HOST, port: Number(process.env.DB_PORT), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: Object.values(entities), synchronize: true, });api/src/types/env.d.ts 声明了完整的ProcessEnv契约DB_HOST、DB_PORT、DB_USERNAME、DB_PASSWORD、DB_DATABASE、JWT_SECRET再加上入口文件读取的PORT缺省 3000。这 7 个变量就是运行 API 所需的全部环境变量。种子数据分两套均复用utils/typeorm的createEntity写入createGuestAccount.ts开发体验种子。依次创建 3 个用户Pickle Rick、Baby Yoda、Lord Gaben、1 个软件类项目singularity 1.0、8 条覆盖各状态/优先级/类型的示例 Issue、8 条俳句风格评论最后返回users[2]作为访客登录账号——这正是POST /authentication/guest背后的完整调用链。createTestAccount.ts最小化测试种子2 用户 / 1 项目 / 3 Issue / 1 评论配合 resetDatabase.tsdropDatabase()synchronize()被 Cypress 测试通过/test/*路由调用实现每轮测试从干净库开始。十一、运行方式与前提结合 api/package.json 的 scriptsAPI 有三种运行模式命令说明npm startnodemon --exec ts-node --files src/index.ts开发热重载npm run start:testcross-env NODE_ENVtest DB_DATABASEjira_test npm start启用/test/*路由并使用独立测试库jira_testnpm run buildcd src tsc按 api/tsconfig.json 编译到buildnpm run start:productionpm2 start --name jira_api node -- -r ./tsconfig-paths.js build/index.js生产模式加载路径别名后由 pm2 托管适用前提本地或可访问的 PostgreSQL 实例已就绪且.env中配好了上文 7 个环境变量由于synchronize: true表结构会随实体定义自动建立无需手工迁移。客户端client/目录React Babel通过 HTTP 消费这套 APICypress 集成测试见 client/cypress/integration则驱动完整的鉴权、建 issue、拖拽看板等流程。十二、小结回到 api/README.md 的原始结构表这套 API 的分层逻辑可以概括为一条清晰的请求链index.ts装配中间件与路由 →middleware/authentication.ts解析 Bearer Token 并挂载currentUser→routes.ts分发到controllers/*→ 控制器调用utils/typeorm.ts完成查找/校验/保存 → 校验规则内联于entities/*的static validations取值域由constants/*枚举锁定 → 结果经serializers/*裁剪 → 全程异常由errors/* 全局错误中间件统一为带code/status/data的 JSON 协议。对希望复刻这种小而全后端的开发者而言本仓库最有借鉴价值的三点是公开/私有路由以一条中间件为界、catchErrorsCustomError白名单收敛全部错误路径、实体声明式validations让每个 CRUD 处理器保持 4~10 行。三者叠加才使得fairly simple不只是一句客套而是可以在 api/src 下逐文件验证的工程事实。赞分享前端后端企业应用【免费下载链接】jira_cloneA simplified Jira clone built with React/Babel (Client), and Node/TypeScript (API). Auto formatted with Prettier, tested with Cypress.项目地址https://gitcode.com/gh_mirrors/ji/jira_clone点击查看免费下载相关推荐Decky Loader终极指南如何在Steam Deck上安装和管理插件Decky Loader终极指南如何在Steam Deck上安装和管理插件 Decky Loader是一款专为Steam Deck设计的开源插件加载器让您能后端前端插件系统HedgeDoc后端架构Nest.js TypeORM实战解析HedgeDoc后端架构Nest.js TypeORM实战解析 本文深入解析了HedgeDoc 2.0后端架构的核心设计与实现重点介绍了Nest.js框后端前端云原生深入解析TypeScript-Node-Starter项目结构深入解析TypeScript Node Starter项目结构 本文深入解析了TypeScript Node Starter项目的整体架构和核心模块设计。文章首后端示例工程上一篇Granite-3B-Code-Base-2K社区贡献指南如何参与开源代码模型的发展下一篇OpCore Simplify 实操教程10 分钟从零到 OpenCore 引导的黑苹果 EFI 配置创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑