DEV Community

Cover image for Modern API Development with Node.js, Express, and TypeScript using Clean Architecture
Dipak Ahirav
Dipak Ahirav

Posted on • Edited on

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

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


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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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" });
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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...
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  });
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create a .env file:

PORT=3000
JWT_SECRET=your_jwt_secret
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Add Prettier configuration:

// .prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (42)

Collapse
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

NodeJS is far from "ideal" for backend. Ideal is the epitome of what backends should be. NodeJS is just OK. But I'm most likely commenting on an AI-generated piece, and we all know what AI is good for: Lies.

Collapse
 
mrrishimeena profile image
Rishi Kumar

Nodejs is far from "ideal" ? Joke of the month.

Collapse
 
nicolus profile image
Nicolus

So you think node.js is the ideal runtime for creating backend APIs ? That's an interesting take.

What makes it better suited for creating APIs than Java or C# in your opinion ?

I've always been under the impression that the rationale for using node.js was "I'm already using javascript for the frontend anyways so I might as well use it for the backend too" rather than "JavaScript is the ultimate backend language, let's all use node.js"

Thread Thread
 
cholasimmons profile image
Chola

If you like .NET, use .NET
Why follow another framework to bash it? ๐Ÿคฃ

Thread Thread
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

Is NodeJS close family or something? You seem offended. Did I touch a nerve or something?

Thread Thread
 
mrrishimeena profile image
Rishi Kumar

@nicolus, it all depends on the use case. I'm not a fan of any one particular language because each has its pros and cons. For service companies, startups, microservices, or chat applications where handling millions of requests simultaneously isn't necessary, any language can work. Node.js, for example, is easy to set up in just a few days, while others may require more time and specialized developers. In the end, it all comes down to the specific use case.

Just because Stack Overflow uses a monolithic application doesn't mean everyone should. Similarly, Discord's transition from MongoDB to Cassandra, and then from Cassandra to Scylla, doesn't mean everyone should start using Scylla. I remember the hype around big data in 2014-15 when everyone wanted to implement it in their applications, even if they only had a few hundred customers. Criticizing a language based on selected criteria is an interesting perspective.

Thread Thread
 
nicolus profile image
Nicolus • Edited

Sure, I (more or less) agree with that, the thing we're discussing in this thread is that sentence from the original post (emphasis mine) :

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

And my follow up question is what makes it particularly "ideal" in the particular situation where you need to build an API that handles many requests ? If your answer is "it's easy to setup" then fine, it's definitely a valid answer, although I personally wouldn't say it makes it "ideal", it just makes it "convenient".

Note that I don't consider it a deal breaker. For example my stack of choice for web projects is PHP with Laravel, and it's most definitely not ideal for anything, it's just incredibly convenient to use and handles 99% of use cases well enough.

Thread Thread
 
mrrishimeena profile image
Rishi Kumar

True.

Collapse
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

The joke is how far from the top spots of performance charts NodeJs is. That's the joke.

Thread Thread
 
mrrishimeena profile image
Rishi Kumar

Unless you wanna handle 100+ billions requests, any language can handle this. you just need better architecture. And 90% of companies don't need such level of requirement.

Thread Thread
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

Incorrect. It depends on things like spikes of usage more than on overall total requests. But you're right in the sense that most systems can handle normal amounts of requests. NodeJs is one of the better ones for "those" cases. That's why I say it is far from ideal, and in the "OK" category.

Thread Thread
 
igorrubinovich profile image
Igor Rubinovich

All resources are limited one way or another, so architecting for scalability is the real concern/goal if indeed operations are expected to spike over that level.
I have a couple of writeups on the topic in my dev.to profile if you will.

Collapse
 
cholasimmons profile image
Chola

I miss the dislike button

Collapse
 
ratulsharker profile image
Ratul sharker • Edited

NodeJS is just a runtime, it is not framework. Itโ€™s not meant to be ideal for backend development.

Like java and spring-boot, it can compared to java, not spring boot. To be ideal, batteries needed to be included. i.e i was scrolling through the post, the author needed to maintain his own DI container. Which is not ideal, as this problem is not new. Business does not care about you maintaining the DI container, all they needed flawless operations.

There might be some controversy, but in my personal opinion, this sort of open ended non-framework implementations can get up up and running pretty fast. Which is ideal for POC and MVP. In the long run, lack of documentation, change in the team member may strangle all the way up. So its all about tooling, which suits the use case in hand.

Collapse
 
tombohub profile image
tombohub

why?

Collapse
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas • Edited

Despite having an efficient event loop that scales pretty well, it cannot possibly beat multi-threaded runtimes like .Net or C/C++ in terms of scalability. For example, drogon is pretty much the king of performance.

But that's just performance-wise. There are other factors, and largely depend on what people need. For example, some people say that because JavaScript is not a typed language, it is difficult to have good IoC containers, and some people just need their IoC container.

Collapse
 
oscardmg profile image
Oscar Montoya

Thank you very much for sharing this post, it is very interesting.

Please can you help me by implementing your code, I have the following error, how would you solve it?

`src/web-api/middleware/auth.ts:14:9 - error TS2339: Property 'user' does not exist on type 'Request>'.

14 req.user = user;`

github.com/oscardmg/nodejs-typescr...

Collapse
 
dipakahirav profile image
Dipak Ahirav • Edited

Thanks @oscardmg for bringing this up. It looks like the error you're encountering is due to a type mismatch when using jwt.verify in TypeScript. The issue typically arises because TypeScript isn't sure about the structure of the decoded token payload. Here's a solution that should resolve the problem:

Solution:

  1. Extend JwtPayload: First, define an interface for your JWT payload that extends JwtPayload. This ensures that the payload type matches what jsonwebtoken expects.

  2. Type Assertion in jwt.verify: Use a type assertion to tell TypeScript what type the decoded token should be.

Here's the updated code:

import jwt, { JwtPayload } from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

// Define the structure of your JWT payload, extending JwtPayload
interface UserPayload extends JwtPayload {
  id: string;
  email: string;
  // Add any other properties you expect in the payload
}

// Extend the Express Request interface to include the user property
interface AuthenticatedRequest extends Request {
  user?: UserPayload;
}

export function authenticateToken(
  req: AuthenticatedRequest,
  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, decoded) => {
    if (err) return res.sendStatus(403);

    // Type assertion to ensure the decoded payload matches UserPayload
    req.user = decoded as UserPayload;
    next();
  });
}
Enter fullscreen mode Exit fullscreen mode

Let me know if you have any further questions or if there's anything else I can help with!

Collapse
 
oscardmg profile image
Oscar Montoya

Thank you very much for the help

Collapse
 
evankhann profile image
Berlin

good writing

Collapse
 
charlesr1971 profile image
Charles Robertson • Edited

This is a good tutorial for getting people started.

I would probably use a framework like NestJS [only because it complements my Angular frontend].

nestjs.com/

It abstracts away some of the features that you have created manually. But your tutorial is valid because it shows how this stuff works, behind the scenes. ๐Ÿ™‚

Well, I use both Coldfusion & NodeJs, for my backend.
The former uses a multithreaded Application Server. This allows a Coldfusion Application Server to handle multiple requests, concurrently.
CFML is generally a linear language, although it is possible to spawn asynchronous threads.
Taffy.io is a good Rest API framework for CFML.
Coldfusion is compiled, at runtime. It is an interpreted language.
NodeJS is single threaded and uses an event loop, but it is fast, very fast, so in fact the asynchronous operations that come off the event loop, create a comparatively fast response, when compared to the multithreaded CFML environment. Although NodeJs can only handle a single request, at any one time, requests are handled very quickly and use asynchronous operations that rejoin the main event loop, to speed up the response times. Surprisingly, NodeJs compares favourably to Coldfusion, when it comes to handling requests. Because NodeJs can process requests faster, this makes up for the fact that it cannot handle concurrency.

Both approaches have their pros & cons, but both systems make good Rest API candidates.

Collapse
 
dipakahirav profile image
Dipak Ahirav

Thank you so much ๐Ÿ˜Š

Collapse
 
benedya profile image
Bohdan Turyk

Hi Dipak, thanks for sharing!
I also like Clean Architecture principles, so it was quite interesting to read.
For DI you might consider using InversifyJS. Maybe it violates the dependency direction rule but it helps a lot with dependencies :)
I saw questions regarding the implementation of this using NestJs. Some time ago I created a skeleton with a very similar approach that you presented here and NestJs. Just in case someone is interested in it here is the link github.com/benedya/nestjs-layered-...
(apologies if sharing links is not allowed, just thought it might be helpful)

Collapse
 
cmohanc profile image
cmohanc

Node.js?is neither clean nor lean.its a combination of dependencies.

Collapse
 
tian_wijaya profile image
tian

on post said, good for testing then provide the DI but on post doesn't create unit testing at all

Collapse
 
jangelodev profile image
Joรฃo Angelo

Hi Dipak Ahirav,
Top, very nice and helpful !
Thanks for sharing.

Collapse
 
dbroadhurst profile image
David Broadhurst

I use NestJS but if I didn't this would be my goto article. Excellent guide and for those crying over node they need to appreciate how well this article describes best practices regardless of the language / framework.

Collapse
 
charlesr1971 profile image
Charles Robertson

I agree.
NestJS is great, because it abstracts away a lot of what this article explains in more detail.
But, it is good to see how Rest API works at a lower level.

Collapse
 
yogithesymbian profile image
Yogi Arif Widodo

awesome