Lädt...


🔧 Modern API Development with Node.js, Express, and TypeScript using Clean Architecture


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

APIs are the backbone of modern web applications. As the complexity of applications grows, it's crucial to adopt an architecture that promotes scalability, maintainability, and testability. In this blog, we'll explore how to build a modern API using Node.js, Express, and TypeScript, all while adhering to Clean Architecture principles.

please subscribe to my YouTube channel to support my channel and get more web development tutorials.

📑 Table of Contents

Sr. No. Section
1. 🧩 Introduction to Clean Architecture
2. 💡 Why Node.js, Express, and TypeScript?
3. 🚧 Setting Up the Project
4. 🏗️ Structuring the Project with Clean Architecture
5. 📂 Implementing the Domain Layer
6. 🔧 Implementing the Use Cases
7. 🗂️ Implementing the Infrastructure Layer
8. 🌐 Implementing the Interface Layer
9. 🔌 Dependency Injection
10. 🚨 Error Handling
11. ✔️ Validation
12. 💾 Real Database Integration
13. 🔒 Authentication and Authorization
14. 📝 Logging and Monitoring
15. ⚙️ Environment Configuration
16. 🚀 CI/CD and Deployment
17. 🧹 Code Quality and Linting
18. 🛠️ Project Documentation
19. 🏁 Conclusion

1. 🧩 Introduction to Clean Architecture

Back to Table of Contents

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), emphasizes the separation of concerns within an application. It promotes the idea that the business logic should be independent of any frameworks, databases, or external systems. This makes the application more modular, easier to test, and adaptable to changes.

Key principles of Clean Architecture:

  • Independence: The core business logic should not depend on external libraries, UI, databases, or frameworks.
  • Testability: The application should be easy to test without relying on external systems.
  • Flexibility: It should be easy to change or replace parts of the application without affecting others.

2. 💡 Why Node.js, Express, and TypeScript?

Back to Table of Contents

Node.js

Node.js is a powerful JavaScript runtime that allows you to build scalable network applications. It's non-blocking and event-driven, making it ideal for building APIs that handle a large number of requests.

Express

Express is a minimalistic web framework for Node.js. It provides a robust set of features for building web and mobile applications and APIs. Its simplicity makes it easy to start with, and it's highly extensible.

TypeScript

TypeScript is a superset of JavaScript that adds static types. Using TypeScript in your Node.js application helps catch errors early in the development process, improves code readability, and enhances the overall developer experience.

3. 🚧 Setting Up the Project

Back to Table of Contents

First, let's create a new Node.js project and set up 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

Next, configure your tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

4. 🏗️ Structuring the Project with Clean Architecture

Back to Table of Contents

A typical Clean Architecture project is divided into the following layers:

  1. Domain Layer: Contains the business logic, entities, and interfaces. This layer is independent of any other layers.
  2. Use Cases Layer: Contains the application's use cases or business rules.
  3. Infrastructure Layer: Contains implementations of the interfaces defined in the domain layer, such as database connections.
  4. Interface Layer: Contains controllers, routes, and any other web framework-related code.

The directory structure might look like this:

src/
├── domain/
│   ├── entities/
│   └── interfaces/
├── use-cases/
├── infrastructure/
│   ├── database/
│   └── repositories/
└── interface/
    ├── controllers/
    └── routes/

5. 📂 Implementing the Domain Layer

Back to Table of Contents

In the domain layer, define your entities and interfaces. Let's say we're building a simple API for managing books.

Entity (Book):

// src/domain/entities/Book.ts
export class Book {
  constructor(
    public readonly id: string,
    public title: string,
    public author: string,
    public publishedDate: Date
  ) {}
}

Repository Interface:

// 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. 🔧 Implementing the Use Cases

Back to Table of Contents

Use cases define the actions that can be performed in the system. They interact with the domain layer and are agnostic to the framework or database used.

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. 🗂️ Implementing the Infrastructure Layer

Back to Table of Contents

In the infrastructure layer, implement the interfaces defined in the domain layer. This is where you interact with databases or external services.

In-Memory Repository (for simplicity):

// 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. 🌐 Implementing the Interface Layer

Back to Table of Contents

The interface layer contains the controllers and routes that handle HTTP requests and map them to 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);
  }
}

Routes:

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

Main Application:

// 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. 🔌 Dependency Injection

Back to Table of Contents

Dependency Injection (DI) is a technique where an object's dependencies are provided rather than hardcoded inside the object. This promotes loose coupling and makes your application easier to test.

Example:

Let's implement a simple DI mechanism using TypeScript.

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

Use the DIContainer in your controllers:

// 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. 🚨 Error Handling

Back to Table of Contents

Proper error handling ensures that your API can gracefully handle unexpected situations and provide meaningful error messages to clients.

Example:

Create a centralized error-handling middleware:

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

Use this middleware in your main application:

// 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. ✔️ Validation

Back to Table of Contents

Validation is crucial for ensuring that the data entering your application is correct and secure.

Example:

Integrate class-validator to validate incoming requests:

npm install class-validator class-transformer

Create a DTO (Data Transfer Object) for book creation:

// src/interface/dto/CreateBookDto.ts
import { IsString, IsDate } from "class-validator";

export class CreateBookDto {
  @IsString()
  title!: string;

  @IsString()
  author!: string;

  @IsDate()
  publishedDate!: Date;
}

Validate the DTO in your controller:

// 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...
  }
}

12. 💾 Real Database Integration

Back to Table of Contents

Switching from an in-memory database to a real database like MongoDB or PostgreSQL makes your application production-ready.

Example:

Integrate MongoDB:

npm install mongoose @types/mongoose

Create a Mongoose model for Book:

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

Implement the repository:

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

Update the DIContainer to use the 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. 🔒 Authentication and Authorization

Back to Table of Contents

Securing your API is essential. JWT (JSON Web Tokens) is a common approach for stateless authentication.

Example:

Integrate JWT for authentication:

npm install jsonwebtoken @types/jsonwebtoken

Create an authentication middleware:

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

Use this middleware to protect routes:

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

14. 📝 Logging and Monitoring

Back to Table of Contents

Logging is crucial for debugging and monitoring your application in production.

Example:

Integrate winston for logging:

npm install winston

Create a logger:

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

Use the logger in your application:

// 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. ⚙️ Environment Configuration

Back to Table of Contents

Managing different environments is crucial for ensuring that your application runs correctly in development, testing, and production.

Example:

Use `

dotenv` for environment configuration:

npm install dotenv

Create a .env file:

PORT=3000
JWT_SECRET=your_jwt_secret

Load environment variables in your application:

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

16. 🚀 CI/CD and Deployment

Back to Table of Contents

Automating the testing, building, and deployment of your API ensures consistency and reliability.

Example:

Set up GitHub Actions for CI/CD:

Create a .github/workflows/ci.yml file:

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. 🧹 Code Quality and Linting

Back to Table of Contents

Maintaining consistent code quality is crucial in collaborative environments.

Example:

Integrate ESLint and Prettier:

npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev

Create an ESLint configuration:

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

Add Prettier configuration:

// .prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80
}

18. 🛠️ Project Documentation

Back to Table of Contents

Documenting your API is crucial for both developers and end-users.

Example:

Generate API documentation with Swagger:

npm install swagger-jsdoc swagger-ui-express

Create Swagger documentation:

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

Set up Swagger in your main application:

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

19. 🏁 Conclusion

Back to Table of Contents

In this blog, we explored how to build a modern API using Node.js, Express, and TypeScript while adhering to Clean Architecture principles. We expanded on the initial implementation by adding key features such as Dependency Injection, Error Handling, Validation, Real Database Integration, Authentication and Authorization, Logging and Monitoring, Environment Configuration, CI/CD, Code Quality and Linting, and Project Documentation.

By following these practices, you'll ensure that your API is not only functional but also maintainable, scalable, and ready for production. As you continue to develop, feel free to explore additional patterns and tools to further enhance your application.

Start Your JavaScript Journey

If you're new to JavaScript or want a refresher, visit my blog on BuyMeACoffee to get started with the basics.

👉 Introduction to JavaScript: Your First Steps in Coding

Series Index

Part Title Link
1 Ditch Passwords: Add Facial Recognition to Your Website with FACEIO Read
2 The Ultimate Git Command Cheatsheet Read
3 Top 12 JavaScript Resources for Learning and Mastery Read
4 Angular vs. React: A Comprehensive Comparison Read
5 Top 10 JavaScript Best Practices for Writing Clean Code Read
6 Top 20 JavaScript Tricks and Tips for Every Developer 🚀 Read
7 8 Exciting New JavaScript Concepts You Need to Know Read
8 Top 7 Tips for Managing State in JavaScript Applications Read
9 🔒 Essential Node.js Security Best Practices Read
10 10 Best Practices for Optimizing Angular Performance Read
11 Top 10 React Performance Optimization Techniques Read
12 Top 15 JavaScript Projects to Boost Your Portfolio Read
13 6 Repositories To Master Node.js Read
14 Best 6 Repositories To Master Next.js Read
15 Top 5 JavaScript Libraries for Building Interactive UI Read
16 Top 3 JavaScript Concepts Every Developer Should Know Read
17 20 Ways to Improve Node.js Performance at Scale Read
18 Boost Your Node.js App Performance with Compression Middleware Read
19 Understanding Dijkstra's Algorithm: A Step-by-Step Guide Read
20 Understanding NPM and NVM: Essential Tools for Node.js Development Read

Follow and Subscribe:

...

🔧 Modern API Development with Node.js, Express, and TypeScript using Clean Architecture


📈 75.69 Punkte
🔧 Programmierung

🔧 Comparing All-in-One Architecture, Layered Architecture, and Clean Architecture


📈 42.99 Punkte
🔧 Programmierung

🔧 Creating a serverless API using AWS Lambda and Node.js with TypeScript and Express.js


📈 41.82 Punkte
🔧 Programmierung

🔧 Clean Architecture in Node.js: An Approach with TypeScript and Dependency Injection.


📈 40.77 Punkte
🔧 Programmierung

🔧 A Modern Approach to Secure APIs with Node.js, Express, TypeScript, and ESM


📈 37.42 Punkte
🔧 Programmierung

🔧 Introduction to the principles of clean architecture in a NodeJs API (Express)


📈 37.2 Punkte
🔧 Programmierung

🔧 User authentication and authorization in Node.js, Express.js app, using Typescript, Prisma, Zod and JWT


📈 36.05 Punkte
🔧 Programmierung

🔧 How to set up an Express API with Node.js and TypeScript the right way in 2024


📈 35.53 Punkte
🔧 Programmierung

🔧 Using Clean Architecture and the Unit of Work Pattern on a Node.js Application


📈 35.24 Punkte
🔧 Programmierung

🔧 The difference between clean code and clean architecture?


📈 34.01 Punkte
🔧 Programmierung

🔧 Clean Architecture: Keeping Code Clean and Maintainable


📈 34.01 Punkte
🔧 Programmierung

🔧 Building a Scalable Furniture E-commerce Web API Using .NET Clean Architecture and MongoDB


📈 33.29 Punkte
🔧 Programmierung

🎥 How to handle API routing with Node.js and Express [19 of 26] | Beginner's Series to Node.js


📈 33.06 Punkte
🎥 Video | Youtube

🎥 How to create a web API with Node.js and Express [17 of 26] | Beginner's Series to Node.js


📈 33.06 Punkte
🎥 Video | Youtube

🔧 How to Fetch Wikipedia Articles Using Node.js (Express, TypeScript, Axios)


📈 32.79 Punkte
🔧 Programmierung

🔧 Setting Up an Express API with TypeScript and Pre-commit Hooks Using Husky


📈 32.47 Punkte
🔧 Programmierung

🔧 Understanding RESTful API Development with Node.js and Express: A Guide to Controller Methods


📈 31.95 Punkte
🔧 Programmierung

🔧 A Beginner's Guide to API Development with Node.js and Express


📈 31.95 Punkte
🔧 Programmierung

🔧 Building a modern gRPC-powered microservice using Node.js, Typescript, and Connect


📈 31.88 Punkte
🔧 Programmierung

🔧 API Development and Microservices: Revolutionizing Modern Software Architecture


📈 31.75 Punkte
🔧 Programmierung

🔧 Implementando Clean Architecture com TypeScript


📈 31.42 Punkte
🔧 Programmierung

🔧 Implementing Clean Architecture with TypeScript


📈 31.42 Punkte
🔧 Programmierung

🔧 Building Mindful: A Mental Wellness App with Node.js, AI, and Clean Architecture 🌿


📈 30.58 Punkte
🔧 Programmierung

🔧 How to Create an API Using Node.js, Express.js, and Axios


📈 30 Punkte
🔧 Programmierung

🔧 Fetching Calendly API Events Using Express and Node.js


📈 30 Punkte
🔧 Programmierung

🔧 Create an e-commerce backend API using Node.js(TypeScript) and MongoDB


📈 29.98 Punkte
🔧 Programmierung

🔧 A Simple Guide to Setting Up TypeScript with Node.js and Express (2024)


📈 29.76 Punkte
🔧 Programmierung

🔧 How to set up an Express Server with Node.js and TypeScript


📈 29.76 Punkte
🔧 Programmierung

🔧 Building a Microservice Architecture with Node.js, TypeScript, and gRPC


📈 29.62 Punkte
🔧 Programmierung

🔧 Mastering NestJS: Unleashing the Power of Clean Architecture and DDD in E-commerce Development — part 1


📈 29.47 Punkte
🔧 Programmierung

🔧 Your First Node Express App with Typescript


📈 28.12 Punkte
🔧 Programmierung

matomo