NodeJS项目架构设计,看这一篇就足够了!
前言
大家好,我是倔强青铜三。我是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。
1. 🧩 整洁架构简介
Clean Architecture(整洁架构)由Robert C. Martin(Uncle Bob)提出,它强调应用程序内部关注点的分离。该架构提倡业务逻辑应与任何框架、数据库或外部系统无关,从而使应用程序更加模块化、易于测试且能够适应变化。
整洁架构的关键原则:
- 独立性:核心业务逻辑不应依赖于外部库、UI、数据库或框架。
- 可测试性:应用程序应易于测试,且不依赖于外部系统。
- 灵活性:应易于更改或替换应用程序的部分,而不影响其他部分。
2. 💡 为什么选择Node.js、Express和TypeScript?
Node.js
Node.js是一个强大的JavaScript运行时,允许你构建可扩展的网络应用程序。它是非阻塞和事件驱动的,非常适合构建需要处理大量请求的API。
Express
Express是Node.js的一个极简主义Web框架。它提供了一套强大的功能来构建Web和移动应用程序及API。其简洁性使得入门容易,且高度可扩展。
TypeScript
TypeScript是JavaScript的一个超集,添加了静态类型。在Node.js应用中使用TypeScript可以在开发早期捕获错误,提高代码可读性,并增强整体开发体验。
3. 🚧 设置项目
首先,创建一个新的Node.js项目并设置TypeScript:
mkdir clean-architecture-api
cd clean-architecture-api
npm init -y
npm install express
npm install typescript @types/node @types/express ts-node-dev --save-dev
npx tsc --init
接下来,配置你的tsconfig.json
:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
4. 🏗️ 使用Clean Architecture构建项目结构
典型的Clean Architecture项目分为以下几层:
- Domain Layer:包含业务逻辑、实体和接口。这一层独立于其他层。
- Use Cases Layer:包含应用程序的用例或业务规则。
- Infrastructure Layer:包含在Domain Layer中定义的接口的实现,如数据库连接。
- Interface Layer:包含控制器、路由和任何其他与Web框架相关的代码。
项目目录结构可能如下所示:
src/
├── domain/
│ ├── entities/
│ └── interfaces/
├── use-cases/
├── infrastructure/
│ ├── database/
│ └── repositories/
└── interface/
├── controllers/
└── routes/
5. 📂 实现Domain Layer
在Domain Layer中定义你的实体和接口。假设我们正在构建一个管理书籍的简单API。
实体(Book):
// src/domain/entities/Book.ts
export class Book {
constructor(
public readonly id: string,
public title: string,
public author: string,
public publishedDate: Date
) {}
}
仓库接口:
// src/domain/interfaces/BookRepository.ts
import { Book } from "../entities/Book";
export interface BookRepository {
findAll(): Promise<Book[]>;
findById(id: string): Promise<Book | null>;
create(book: Book): Promise<Book>;
update(book: Book): Promise<void>;
delete(id: string): Promise<void>;
}
6. 🔧 实现Use Cases
Use Cases定义了系统中可以执行的操作。它们与Domain Layer交互,并且与框架或数据库无关。
Use Case(GetAllBooks):
// src/use-cases/GetAllBooks.ts
import { BookRepository } from "../domain/interfaces/BookRepository";
export class GetAllBooks {
constructor(private bookRepository: BookRepository) {}
async execute() {
return await this.bookRepository.findAll();
}
}
7. 🗂️ 实现Infrastructure Layer
在Infrastructure Layer中实现Domain Layer中定义的接口。这是与数据库或外部服务交互的地方。
内存仓库(为了简化):
// src/infrastructure/repositories/InMemoryBookRepository.ts
import { Book } from "../../domain/entities/Book";
import { BookRepository } from "../../domain/interfaces/BookRepository";
export class InMemoryBookRepository implements BookRepository {
private books: Book[] = [];
async findAll(): Promise<Book[]> {
return this.books;
}
async findById(id: string): Promise<Book | null> {
return this.books.find(book => book.id === id) || null;
}
async create(book: Book): Promise<Book> {
this.books.push(book);
return book;
}
async update(book: Book): Promise<void> {
const index = this.books.findIndex(b => b.id === book.id);
if (index !== -1) {
this.books[index] = book;
}
}
async delete(id: string): Promise<void> {
this.books = this.books.filter(book => book.id !== id);
}
}
8. 🌐 实现Interface Layer
Interface Layer包含处理HTTP请求并将它们映射到Use Cases的控制器和路由。
Book Controller:
// src/interface/controllers/BookController.ts
import { Request, Response } from "express";
import { GetAllBooks } from "../../use-cases/GetAllBooks";
export class BookController {
constructor(private getAllBooks: GetAllBooks) {}
async getAll(req: Request, res: Response) {
const books = await this.getAllBooks.execute();
res.json(books);
}
}
路由:
// src/interface/routes/bookRoutes.ts
import { Router } from "express";
import { InMemoryBookRepository } from "../../infrastructure/repositories/InMemoryBookRepository";
import { GetAllBooks } from "../../use-cases/GetAllBooks";
import { BookController } from "../controllers/BookController";
const router = Router();
const bookRepository = new InMemoryBookRepository();
const getAllBooks = new GetAllBooks(bookRepository);
const bookController = new BookController(getAllBooks);
router.get("/books", (req, res) => bookController.getAll(req, res));
export { router as bookRoutes };
主应用程序:
// src/index.ts
import express from "express";
import { bookRoutes } from "./interface/routes/bookRoutes";
const app = express();
app.use(express.json());
app.use("/api", bookRoutes);
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
9. 🔌 依赖注入
依赖注入(DI)是一种技术,其中对象的依赖项由外部提供,而不是硬编码在对象内部。这促进了松散耦合,并使你的应用程序更易于测试。
示例:
// src/infrastructure/DIContainer.ts
import { InMemoryBookRepository } from "./repositories/InMemoryBookRepository";
import { GetAllBooks } from "../use-cases/GetAllBooks";
class DIContainer {
private static _bookRepository = new InMemoryBookRepository();
static getBookRepository() {
return this._bookRepository;
}
static getGetAllBooksUseCase() {
return new GetAllBooks(this.getBookRepository());
}
}
export { DIContainer };
在控制器中使用DIContainer:
// src/interface/controllers/BookController.ts
import { Request, Response } from "express";
import { DIContainer } from "../../infrastructure/DIContainer";
export class BookController {
private getAllBooks = DIContainer.getGetAllBooksUseCase();
async getAll(req: Request, res: Response) {
const books = await this.getAllBooks.execute();
res.json(books);
}
}
10. 🚨 错误处理
适当的错误处理可以确保你的API能够优雅地处理意外情况,并向客户端提供有意义的错误消息。
示例:
// src/interface/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
console.error(err.stack);
res.status(500).json({ message: "Internal Server Error" });
}
在主应用程序中使用错误处理中间件:
// src/index.ts
import express from "express";
import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
const app = express();
app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
11. ✔️ 验证
验证对于确保进入应用程序的数据正确且安全至关重要。
示例:
npm install class-validator class-transformer
创建用于书籍创建的DTO:
// src/interface/dto/CreateBookDto.ts
import { IsString, IsDate } from "class-validator";
export class CreateBookDto {
@IsString()
title!: string;
@IsString()
author!: string;
@IsDate()
publishedDate!: Date;
}
在控制器中验证DTO:
// src/interface/controllers/BookController.ts
import { Request, Response } from "express";
import { validate } from "class-validator";
import { CreateBookDto } from "../dto/CreateBookDto";
import { DIContainer } from "../../infrastructure/DIContainer";
export class BookController {
private getAllBooks = DIContainer.getGetAllBooksUseCase();
async create(req: Request, res: Response) {
const dto = Object.assign(new CreateBookDto(), req.body);
const errors = await validate(dto);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// 继续创建逻辑...
}
}
12. 💾 真实数据库集成
将内存数据库切换到如MongoDB或PostgreSQL等真实数据库,可以使你的应用程序准备好投入生产。
示例:
npm install mongoose @types/mongoose
为Book
创建Mongoose模型:
// src/infrastructure/models/BookModel.ts
import mongoose, { Schema, Document } from "mongoose";
interface IBook extends Document {
title: string;
author: string;
publishedDate: Date;
}
const BookSchema: Schema = new Schema({
title: { type: String, required: true },
author: { type: String, required: true },
publishedDate: { type: Date, required: true },
});
const BookModel = mongoose.model<IBook>("Book", BookSchema);
export { BookModel, IBook };
实现仓库:
// src/infrastructure/repositories/MongoBookRepository.ts
import { Book } from "../../domain/entities/Book";
import { BookRepository } from "../../domain/interfaces/BookRepository";
import { BookModel } from "../models/BookModel";
export class MongoBookRepository implements BookRepository {
async findAll(): Promise<Book[]> {
return await BookModel.find();
}
async findById(id: string): Promise<Book | null> {
return await BookModel.findById(id);
}
async create(book: Book): Promise<Book> {
const newBook = new BookModel(book);
await newBook.save();
return newBook;
}
async update(book: Book): Promise<void> {
await BookModel.findByIdAndUpdate(book.id, book);
}
async delete(id: string): Promise<void> {
await BookModel.findByIdAndDelete(id);
}
}
更新DIContainer以使用MongoBookRepository:
// src/infrastructure/DIContainer.ts
import { MongoBookRepository } from "./repositories/MongoBookRepository";
import { GetAllBooks } from "../use-cases/GetAllBooks";
class DIContainer {
private static _bookRepository = new MongoBookRepository();
static getBookRepository() {
return this._bookRepository;
}
static getGetAllBooksUseCase() {
return new GetAllBooks(this.getBookRepository());
}
}
export { DIContainer };
13. 🔒 身份验证和授权
保护你的API至关重要。JWT(JSON Web Tokens)是一种常用的无状态身份验证方法。
示例:
npm install jsonwebtoken @types/jsonwebtoken
创建一个身份验证中间件:
// src/interface/middleware/auth.ts
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
export function authenticateToken(req: Request, res: Response, next: NextFunction) {
const token = req.header("Authorization")?.split(" ")[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET as string, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
在路由中使用此中间件来保护路由:
// src/interface/routes/bookRoutes.ts
import { Router } from "express";
import { BookController } from "../controllers/BookController";
import { authenticateToken } from "../middleware/auth";
const router = Router();
const bookController = new BookController();
router.get("/books", authenticateToken, (req, res) => bookController.getAll(req, res));
export { router as bookRoutes };
14. 📝 日志记录和监控
日志记录在调试和生产环境中监控应用程序时至关重要。
示例:
npm install winston
创建一个记录器:
// src/infrastructure/logger.ts
import { createLogger, transports, format } from "winston";
const logger = createLogger({
level: "info",
format: format.combine(format.timestamp(), format.json()),
transports: [new transports.Console()],
});
export { logger };
在应用程序中使用记录器:
// src/index.ts
import express from "express";
import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";
const app = express();
app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server is running on port ${PORT}`);
});
15. ⚙️ 环境配置
管理不同的环境对于确保你的应用程序在开发、测试和生产环境中正确运行至关重要。
示例:
npm install dotenv
创建一个.env
文件:
PORT=3000
JWT_SECRET=your_jwt_secret
在应用程序中加载环境变量:
// src/index.ts
import express from "express";
import dotenv from "dotenv";
import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";
dotenv.config();
const app = express();
app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server is running on port ${PORT}`);
});
16. 🚀 CI/CD和部署
自动化API的测试、构建和部署可以确保一致性和可靠性。
示例:
创建一个.github/workflows/ci.yml
文件:
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
17. 🧹 代码质量和Linting
在协作环境中保持一致的代码质量至关重要。
示例:
npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev
创建ESLint配置:
// .eslintrc.json
{
"env": {
"node": true,
"es6": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"plugins": ["@typescript-eslint", "prettier"],
"parser": "@typescript-eslint/parser",
"rules": {
"prettier/prettier": "error"
}
}
添加Prettier配置:
// .prettierrc
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80
}
18. 🛠️ 项目文档
为你的API编写文档对于开发人员和最终用户都至关重要。
示例:
npm install swagger-jsdoc swagger-ui-express
创建Swagger文档:
// src/interface/swagger.ts
import swaggerJSDoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
import { Express } from "express";
const options = {
definition: {
openapi: "3.0.0",
info: {
title: "Clean Architecture API",
version: "1.0.0",
},
},
apis: ["./src/interface/routes/*.ts"],
};
const swaggerSpec = swaggerJSDoc(options);
function setupSwagger(app: Express) {
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
}
export { setupSwagger };
在主应用程序中设置Swagger:
// src/index.ts
import express from "express";
import dotenv from "dotenv";
import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";
import { setupSwagger } from "./interface/swagger";
dotenv.config();
const app = express();
app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);
setupSwagger(app);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server is running on port ${PORT}`);
});
19. 🏁 结论
在本博客中,我们探讨了如何使用Node.js、Express和TypeScript构建现代API,同时遵循整洁架构原则。我们扩展了初始实现,添加了关键功能,如依赖注入、错误处理、验证、真实数据库集成、身份验证和授权、日志记录和监控、环境配置、CI/CD、代码质量和Linting以及项目文档。
通过遵循这些实践,你将确保你的API不仅功能齐全,而且易于维护、可扩展且准备好投入生产。随着你继续开发,请随时探索其他模式和工具,以进一步增强你的应用程序。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。