API 是現代 Web 應用程式的支柱。隨著應用程式複雜性的增加,採用可提高可擴展性、可維護性和可測試性的架構至關重要。在本部落格中,我們將探索如何使用 Node.js、Express 和 TypeScript 建立現代 API,同時遵守簡潔架構原則。

請訂閱我的 [YouTube 頻道](https://www.youtube.com/@DevDivewithDipak?sub\_confirmation=1

)來支援我的頻道並獲取更多 Web 開發教學。

📑 目錄

|先生號|部分|

|---------|---------|

| 1.| 🧩 清潔架構簡介|

| 2.| 💡 為什麼選擇 Node.js、Express 和 TypeScript? |

| 3.| 🚧 設定專案|

| 4.| 🏗️ 用簡潔的架建置構專案|

| 5.| 📂 實作領域層|

| 6.| 🔧 實作用例|

| 7.| 🗂️ 實施基礎設施層|

| 8.| 🌐 實作介面層|

| 9.| 🔌 依賴注入|

| 10.| 🚨 錯誤處理|

| 11.| ✔️ 驗證|

| 12.| 💾 真正的資料庫整合|

| 13.| 🔒 身份驗證與授權|

| 14.| 📝 日誌記錄與監控|

| 15.| ⚙️環境配置|

| 16.| 🚀 CI/CD 和部署|

| 17.| 🧹 程式碼品質與 Linting |

| 18.| 🛠️ 專案文件|

| 19.| 🏁 結論|


  1. 🧩 清潔架構簡介

回目錄

清潔架構由 Robert C. Martin(鮑伯叔叔)提出,強調應用程式中關注點的分離。它提倡這樣一種理念:業務邏輯應該獨立於任何框架、資料庫或外部系統。這使得應用程式更加模組化、更易於測試並且能夠適應變化。

清潔架構的關鍵原則:

  • 獨立性:核心業務邏輯不應依賴外部函式庫、UI、資料庫或框架。

  • 可測試性:應用程式應該易於測試,無需依賴外部系統。

  • 靈活性:應該易於更改或替換應用程式的某些部分,而不影響其他應用程式。

  1. 💡 為什麼選擇 Node.js、Express 和 TypeScript?

回目錄

Node.js

Node.js 是一個功能強大的 JavaScript 執行時,可讓您建立可擴展的網頁應用程式。它是非阻塞且事件驅動的,非常適合建立處理大量請求的 API。

表達

Express 是 Node.js 的簡約 Web 框架。它提供了一組強大的功能來建立 Web 和行動應用程式以及 API。它的簡單性使其易於上手,並且具有高度可擴展性。

打字稿

TypeScript 是 JavaScript 的超集,新增了靜態型別。在 Node.js 應用程式中使用 TypeScript 有助於在開發過程的早期捕獲錯誤、提高程式碼可讀性並增強整體開發人員體驗。

  1. 🚧 設定專案

回目錄

首先,我們建立一個新的 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"]
}
  1. 🏗️ 用乾淨的架建置構專案

回目錄

一個典型的清潔架構專案分為以下幾層:

  1. 領域層:包含業務邏輯、實體和介面。該層獨立於任何其他層。

  2. 用例層:包含應用程式的用例或業務規則。

  3. 基礎設施層:包含領域層中定義的介面的實現,例如資料庫連接。

  4. 介面層:包含控制器、路由和任何其他與 Web 框架相關的程式碼。

目錄結構可能如下所示:

src/
├── domain/
│   ├── entities/
│   └── interfaces/
├── use-cases/
├── infrastructure/
│   ├── database/
│   └── repositories/
└── interface/
    ├── controllers/
    └── routes/
  1. 📂 實作領域層

回目錄

在網域層中,定義您的實體和介面。假設我們正在建立一個簡單的 API 來管理書籍。

實體(書籍):

// 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>;
}
  1. 🔧 實作用例

回目錄

用例定義了可以在系統中執行的操作。它們與領域層交互,並且與所使用的框架或資料庫無關。

用例(獲取所有書籍):

// 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();
  }
}
  1. 🗂️ 實施基礎設施層

回目錄

在基礎設施層中,實現領域層中定義的介面。這是您與資料庫或外部服務互動的地方。

記憶體儲存庫(為了簡單起見):

// 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);
  }
}
  1. 🌐 實作介面層

回目錄

介面層包含處理 HTTP 請求並將其對應到用例的控制器和路由。

書籍控制器:

// 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}`);
});
  1. 🔌依賴注入

回目錄

依賴注入 (DI) 是一種提供物件依賴項而不是硬編碼在物件內部的技術。這促進了鬆散耦合並使您的應用程式更易於測試。

例子:

讓我們使用 TypeScript 實作一個簡單的 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);
  }
}
  1. 🚨 錯誤處理

回目錄

正確的錯誤處理可確保您的 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.✔️驗證

回目錄

驗證對於確保輸入應用程式的資料正確且安全至關重要。

例子:

整合class-validator來驗證傳入的請求:

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 });
    }

    // Proceed with the creation logic...
  }
}
  1. 💾 真正的資料庫集成

回目錄

從記憶體資料庫切換到 MongoDB 或 PostgreSQL 等真實資料庫可以讓您的應用程式做好生產準備。

例子:

整合 MongoDB:

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 };
  1. 🔒 身份驗證與授權

回目錄

保護您的 API 至關重要。 JWT(JSON Web 令牌)是無狀態身份驗證的常用方法。

例子:

整合 JWT 進行身份驗證:

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();

router.get("/books", authenticateToken, (req, res) => bookController.getAll(req, res));

export { router as bookRoutes };
  1. 📝 日誌記錄和監控

回目錄

日誌記錄對於除錯和監控生產中的應用程式至關重要。

例子:

整合winston進行日誌記錄:

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.⚙️環境配置

回目錄

管理不同的環境對於確保應用程式在開發、測試和生產中正確運作至關重要。

例子:

使用`

dotenv` 用於環境配置:

npm install dotenv

建立一個.env檔:

PORT=3000
JWT_SECRET=your_jwt_secret

在您的應用程式中載入環境變數:

// src/index.ts
import express from "express";
import dotenv from "dotenv";
dotenv.config();

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}`);
});
  1. 🚀 CI/CD 和部署

回目錄

自動測試、建置和部署 API 可確保一致性和可靠性。

例子:

為 CI/CD 設定 GitHub Actions:

建立.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
  1. 🧹 程式碼品質和 Linting

回目錄

在協作環境中保持一致的程式碼品質至關重要。

例子:

整合 ESLint 和 Prettier:

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"
  }
}

增加更漂亮的配置:

// .prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80
}
  1. 🛠️專案文件

回目錄

記錄 API 對於開發人員和最終用戶都至關重要。

例子:

使用 Swagger 產生 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";
dotenv.config();

import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";
import { setupSwagger } from "./interface/swagger";

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}`);
});
  1. 🏁 結論

回目錄

在本部落格中,我們探討如何使用 Node.js、Express 和 TypeScript 建立現代 API,同時遵守簡潔架構原則。我們透過加入依賴注入、錯誤處理、驗證、真實資料庫整合、身份驗證和授權、日誌記錄和監控、環境配置、CI/CD、程式碼品質和 Linting 以及專案文件等關鍵功能來擴展最初的實作。

透過遵循這些實踐,您將確保您的 API 不僅功能齊全,而且可維護、可擴展並可用於生產。隨著您繼續開發,請隨意探索其他模式和工具以進一步增強您的應用程式。

開始你的 JavaScript 之旅

如果您是 JavaScript 新手或想要複習一下,請存取我在 BuyMeACoffee 上的部落格以開始了解基礎知識。

👉 JavaScript 簡介:編碼的第一步

![](https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=☕&slug=dipakahirav&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff)

系列索引

|部分|標題 |連結 |

|------|------------------------------------------------ -------------- -----------|------------------------ ---------------- --------|

| 1 |拋棄密碼:使用 FACEIO 將臉部辨識加入您的網站 | 閱讀|

| 2 |終極 Git 指令備忘單 |閱讀|

| 3 | 12 個用於學習與掌握的 JavaScript 資源 |閱讀|

| 4 | Angular 與 React:全面比較 |閱讀|

| 5 |編寫簡潔程式碼的 10 個 JavaScript 最佳實務 | 閱讀|

| 6 |適合每位開發人員的 20 個 JavaScript 技巧與技巧🚀 |閱讀|

| 7 |您需要了解的 8 個令人興奮的新 JavaScript 概念 |閱讀|

| 8 |在 JavaScript 應用程式中管理狀態的 7 個重要技巧 | 閱讀|

| 9 | 🔒 基本 Node.js 安全最佳實務 |閱讀|

| 10 | 10優化角度效能的 10 個最佳實踐 |閱讀|

| 11 | 11十大 React 效能優化技術 |閱讀|

| 12 | 12增強您的作品集的 15 個最佳 JavaScript 專案 |閱讀|

| 13 |掌握 Node.js 的 6 個儲存庫 |閱讀|

| 14 | 14掌握 Next.js 的 6 個最佳儲存庫 |閱讀|

| 15 | 15用於建立互動式 UI 的 5 個最佳 JavaScript 庫 |閱讀|

| 16 | 16每個開發人員都應該了解的 3 個 JavaScript 概念 |閱讀|

| 17 | 17大規模提高 Node.js 效能的 20 種方法 |閱讀|

| 18 | 18使用壓縮中間件提升 Node.js 應用程式效能 | 閱讀|

| 19 | 19了解 Dijkstra 演算法:逐步指南 |閱讀|

| 20 |了解 NPM 和 NVM:Node.js 開發的基本工具 | 閱讀|

關注並訂閱:


原文出處:https://dev.to/dipakahirav/modern-api-development-with-nodejs-express-and-typescript-using-clean-architecture-1m77


共有 0 則留言