DEV Community

Cover image for A Simple Way to Organize API Endpoints Like a Senior
behnam rahimpour
behnam rahimpour

Posted on • Edited on

A Simple Way to Organize API Endpoints Like a Senior

Table of Contents

Overview

API URLs, versions, and endpoints are among the most volatile parts of any application. Since they depend on external services, frequent changes can quickly lead to:

  • Spaghetti code (scattered URL strings)
  • Brittle integrations (tight coupling with third-party APIs)
  • Maintenance nightmares (manual updates across files)

This architecture solves those problems by:

  • Centralizing endpoint logic in dedicated classes.
  • Abstracting URL construction behind reusable methods.
  • Isolating versioning/base URLs for easy updates.

Result: Your core code stays clean, even when APIs change.

This article is a part of set of articles about clean architecture in Next.js, to explore all concepts in depth and see a production-ready boilerplate and following these best practices, visit:

GitHub logo behnamrhp / Next-clean-boilerplate

A Full featured nextjs boilerplate, based on clean architecture, mvvm and functional programming paradigm.

Nextjs clean architecture boilerplate

Table of content

Overview

This project is a starting point for your medium to large scale projects with Nextjs, to make sure having a structured, maintainable and scalable foundation for your Next.js project based on best practices in clean architecture, DDD approach for business logics, MVVM for the frontend part, storybook and vitest for testing and, localization and also functional programming with error handling for business logics.

Motivation

Next.js and many other modern SSR frameworks provide powerful tools and a fresh approach to frontend development. However, since these tools are relatively new, the focus has largely been on features rather than software engineering best practices.

As a result, many teams use Next.js for its capabilities but neglect maintainability, architecture, and scalability—especially in medium to large-scale applications…

Endpoints architecture

The following class diagram shows the architecture of endpoints:
EndpointArchitecture

Endpoints explanations

BaseEndpoint:

The Endpoint class is implemented for manipulating API endpoints within an application.
the Endpoint class requires two parameters: baseURL and apiVersion, which represent the base URL of the API and the version of the API being used.

export default class Endpoint {
  protected baseURL: string;

  protected apiVersion: string;

  constructor({
    baseURL,
    apiVersion,
  }: {
    baseURL: string;
    apiVersion: string;
  }) {
    this.apiVersion = apiVersion;
    this.baseURL = baseURL;
  }

  static compose(uris: string[]) {
    return Endpoint.sanitizeURL(uris.join("/"));
  }

  protected buildEndpoint(endpoint: string) {
    return Endpoint.sanitizeURL(
      `${this.baseURL}/${this.apiVersion}/${endpoint}`,
    );
  }

  static sanitizeURL(url: string) {
    return url.replaceAll(/(?<!:)\/\//g, "/");
  }
}
Enter fullscreen mode Exit fullscreen mode

Endpoint class has several methods:

  • compose Method (Static) This static method, takes an array of strings representing different parts of the URL and returns a sanitized URL by joining these parts together. It ensures that the URL is properly formatted and free of any unnecessary double slashes or incorrect separators.
  • buildEndpoint Method This instance method, buildEndpoint, is responsible for constructing a complete endpoint URL by appending a specific endpoint to the base URL and API version. It returns a sanitized URL string ready for use in API requests.
  • sanitizeURL Method (Static) You can use this method to ensure that the constructed URLs are correctly formatted. It replaces any occurrences of consecutive slashes with a single slash, ensuring that the URL is valid and compliant with URL standards.

Overall, The Endpoint class offers a convenient and reliable way to construct and manage endpoint URLs within an application. It enables team members to use a shared language when working with third-party libraries, reducing the risk of bloating the main application code due to API or UI changes in external dependencies.

SpecificEndpoint

Every specific endpoint class extends the functionality of the Endpoint class and introduces additional features specific to this specific endpoint. This inheritance includes methods for composing and building endpoint URLs.
The specific endpoint encapsulates private properties, these properties are URLs for a specific endpoint.
The following is an example about making a specific endpoint and how it extends the base endpoint:

export default class BookEndpoint extends Endpoint {
  private addBookEndpoint: string;

  private booksEndpoint: string;

  get addBook() {
    return this.buildEndpoint(this.addBookEndpoint);
  }

  get books() {
    return this.buildEndpoint(this.booksEndpoint);
  }

  constructor({
    addBookEndpoint,
    booksEndpoint,
    baseURL,
    apiVersion,
  }: {
    addBookEndpoint: string;
    booksEndpoint: string;
    baseURL: string;
    apiVersion: string;
  }) {
    super({
      apiVersion,
      baseURL,
    });
    this.addBookEndpoint = addBookEndpoint;
    this.booksEndpoint = booksEndpoint;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you see the BookEndpoint class extends Endpoint to provide a structured way of managing book-related API endpoints. It encapsulates endpoint construction logic, ensuring consistency and reusability across the application.

EndpointsProvider

The EndpointsProvider class offers centralized static methods for retrieving various endpoints used in the application. Each method returns an instance of a specific endpoint class, preconfigured with the necessary URLs and settings.

By encapsulating the logic for creating and configuring endpoints, this class simplifies access and usage across the application. Below is an example of how the EndpointsProvider works:

import BookEndpoint from "~/bootstrap/helper/endpoint/endpoints/book-endpoints";
import appConfigs from "~/bootstrap/config/app-configs";

/**
 * Provides static methods to retrieve different types of endpoints.
 */
export default class EndpointsProvider {
  /**
   * Book api
   */
  static book() {
    return new BookEndpoint({
      apiVersion: "api/v1",
      addBookEndpoint: "book/add",
      booksEndpoint: "book",
      baseURL: appConfigs.baseApis.book,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: The EndpointProvider step is optional to make the code agnostic about the changes or replacing of BookEndpoint with any other one. Instead, you can statically define all required endpoint configurations directly in the endpoint class's constructor. It's your choice based on the flexibility that your projects need.

Usage Example

You can see implementation examples in this Next.js boilerplate:

Endpoint Files

Implementation

  • Repository usage Demonstrates how backend endpoints are consumed throughout the application

Conclusion

Managing API endpoints effectively is crucial for building maintainable applications in our API-driven world. The architecture we've explored:

Isolates volatile API configurations from business logic

Standardizes endpoint management across your codebase

Dramatically reduces tech debt when APIs inevitably change

By implementing this pattern, you'll spend less time on finding and fixing broken endpoints and more time building features by separating api management responsibility in an isolated layer.

Remember: Good architecture isn't about preventing change - it's about making change manageable.

If you found this article helpful, I’d be truly grateful if you could:
⭐ Star the repository to support its visibility
💬 Share your thoughts in the comments—your feedback helps improve content reach

Every interaction helps these best practices reach more developers!

Top comments (0)