Developing an API for multiple clients presents a unique challenge. While core endpoints like /users
are common across all clients, some features may require individual customization. For instance, one client (e.g. "User A") may request a specialized endpoint like /groups
, exclusive to their use only. Additionally, each client will have their own database schema due to these custom features.
In my case, I prefer using NestJs with Express. The app.module
currently manages all core modules and their respective endpoints.
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module'; // core module
@Module({
imports: [UsersModule]
})
export class AppModule {}
This issue isn't specifically related to NestJs, so how would you theoretically address this situation?
The solution requires an infrastructure that can support a basic system. Instead of having predefined core endpoints, the application should be able to adapt to unique extensions introduced by different clients. New features should seamlessly integrate without altering the core application. Extensions must self-integrate or be integrated upon startup. The core system itself won't have any predefined endpoints but will be extended externally.
Several approaches come to mind:
First approach:
Each extension acts as a new repository within a custom external folder dedicated to housing these extension projects. This directory contains separate folders for each extension, such as a groups
folder with its own groups.module
.
import { Module } from '@nestjs/common';
import { GroupsController } from './groups.controller';
@Module({
controllers: [GroupsController],
})
export class GroupsModule {}
The API could scan through this directory and import each module file accordingly.
pros:
- Keeps custom code separate from the core repository.
cons:
- You need to compile Typescript files as NestJs uses Typescript. How would you manage building the API alongside the custom app builds? (Plug-and-play system)
- Custom extensions are somewhat isolated, lacking access to the main API's node_modules directory. This might lead to editor errors due to unresolved package dependencies.
- Certain extensions may require data from other extensions, which complicates inter-extension communication.
Second approach:
Store each extension within a subfolder of the API's src directory while adding it to the .gitignore file. This allows you to keep extensions within the API.
pros:
- Editor can resolve dependencies more effectively.
- Prior to deployment, running the build command results in a unified distribution.
- Accessing other services is simplified (e.g.
/groups
interacting with user data).
cons:
- While developing, you must copy repository files into the subfolder. After making changes, you'll need to override the repository files with updated versions.
Third approach:
In an external custom folder, consider treating each extension as a standalone API. The main API would handle authentication and serve as a proxy to route incoming requests to the designated API.
pros:
- Eases development and testing of new extensions.
cons:
- Deployment complexities arise from having a main API and multiple independent extension APIs operating on various ports.
- Implementing a reliable proxy system is crucial, especially when redirecting requests based on specific endpoints.
- To secure extension APIs, the proxy must share a secret key with these APIs to validate incoming requests.
Fourth approach:
Microservices offer another viable option. Utilizing a guide like the one found here https://docs.nestjs.com/microservices/basics, you could create microservices for components like user management and group management. These services can be consumed by a gateway or proxy configured within the API.
pros:
- Simplifies development and testing of new features.
- Establishes clear separation of concerns.
cons:
- Deploying a main API along with multiple microservices introduces complexity, each potentially listening on separate ports.
- Creating a customized consuming API for each customer may not fully address the initial problem of extending the application efficiently.
- Securing extension APIs requires collaboration between the proxy and the targeted APIs using shared secrets for validation.