Enhancing React Frontend Architecture with Domain-Driven Design and TypeScript Using Neverthrow

Elevate your React applications with Domain-Driven Design (DDD)! This engaging guide explains how to integrate DDD using TypeScript and Neverthrow for better error handling. Dive into our practical use case with detailed code examples and folder organization.

Enhancing React Frontend Architecture with Domain-Driven Design and TypeScript Using Neverthrow
Photo by Artem Kniaz / Unsplash

Domain-Driven Design (DDD) offers a strategic approach to developing complex software systems, emphasizing a deep focus on the core domain and its logic. When applied to frontend development, particularly with React and TypeScript, DDD facilitates a well-organized codebase that is easy to manage and scales gracefully. By using TypeScript along with Neverthrow, a library for handling errors in a functional way, developers can further enhance the robustness and maintainability of their applications.

Use Case: Online E-commerce Store

In this article, we'll implement DDD in a React project for an online e-commerce store. This store includes features like product listings, a shopping cart, user profiles, and order management.

Folder Structure

A clear folder structure helps separate the domain logic from application logic, adhering to DDD principles:

/src
  /domain
    /product
      product.ts
      productService.ts
    /cart
      cart.ts
      cartService.ts
    /user
      user.ts
      userService.ts
  /infrastructure
    /api
      productApi.ts
      userApi.ts
      cartApi.ts
  /ui
    /components
      ProductList.tsx
      Cart.tsx
      UserProfile.tsx
    /pages
      HomePage.tsx
      ProductPage.tsx
      CartPage.tsx

Domain Model Example

Below is a TypeScript example of a domain model for a product:

// src/domain/product/product.ts
import { err, ok, Result } from 'neverthrow';

export interface Product {
    id: string;
    name: string;
    price: number;
    description: string;
    imageUrl: string;
}

export class Product {
    constructor(
        public id: string,
        public name: string,
        public price: number,
        public description: string,
        public imageUrl: string
    ) {}

    applyDiscount(discountPercentage: number): Result<void, Error> {
        if (discountPercentage <= 0 || discountPercentage > 100) {
            return err(new Error('Invalid discount percentage'));
        }
        this.price = this.price * (1 - discountPercentage / 100);
        return ok();
    }
}

Services

Services manage the business logic related to domain entities. Here’s how a product service might look using Neverthrow for better error handling:

// src/domain/product/productService.ts
import { Product } from './product';
import { productApi } from '../../infrastructure/api/productApi';
import { Result } from 'neverthrow';

export class ProductService {
    async getAllProducts(): Promise<Result<Product[], Error>> {
        return await productApi.fetchProducts();
    }

    async getProductById(id: string): Promise<Result<Product, Error>> {
        return await productApi.fetchProductById(id);
    }
}

Product API with Neverthrow

Now, let's implement the productApi with error handling using Neverthrow:

// src/infrastructure/api/productApi.ts
import { Product } from '../../domain/product/product';
import { err, ok, Result } from 'neverthrow';

const API_URL = 'https://api.yourdomain.com/products';

export const productApi = {
    async fetchProducts(): Promise<Result<Product[], Error>> {
        try {
            const response = await fetch(`${API_URL}/`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            });
            if (!response.ok) {
                return err(new Error('Failed to fetch products'));
            }
            const products: Product[] = await response.json();
            return ok(products);
        } catch (error) {
            return err(new Error('Network error occurred while fetching products'));
        }
    },

    async fetchProductById(id: string): Promise<Result<Product, Error>> {
        try {
            const response = await fetch(`${API_URL}/${id}`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            });
            if (!response.ok) {
                return err(new Error(`Failed to fetch product with id ${id}`));
            }
            const product: Product = await response.json();
            return ok(product);
        } catch (error) {
            return err(new Error(`Network error occurred while fetching product with id ${id}`));
        }
    }
};

Conclusion

Incorporating Domain-Driven Design into your React applications using TypeScript and Neverthrow can significantly improve the structure, scalability, and error handling of your codebase. By aligning your software development with business needs,

DDD helps deliver solutions that are not only technically sound but also strategically focused. Start using these principles and tools to build robust and business-oriented frontend applications.

Happy coding!