extend existing API with custom endpoints

Tags: , , , ,



I’m creating an API for multiple customers. The core endpoints like /users are used by every customer but some endpoints rely on individual customization. So it might be that User A wants a special endpoint /groups and no other customer will have that feature. Just as a sidenote, each customer would also use his own database schema because of those extra features.

I personally use NestJs (Express under the hood). So the app.module currently registers all my core modules (with their own endpoints etc.)

import { Module } from '@nestjs/common';

import { UsersModule } from './users/users.module'; // core module

@Module({
  imports: [UsersModule]
})
export class AppModule {}

I think this problem is not related to NestJs so how would you handle that in theory?

I basically need an infrastructure that is able to provide a basic system. There are no core endpoints anymore because each extension is unique and multiple /users implementations could be possible. When developing a new feature the core application should not be touched. Extensions should integrate themselves or should get integrated on startup. The core system ships with no endpoints but will be extended from those external files.

Some ideas come to my mind


First approach:

Each extension represents a new repository. Define a path to a custom external folder holding all that extension projects. This custom directory would contain a folder groups with a groups.module

import { Module } from '@nestjs/common';

import { GroupsController } from './groups.controller';

@Module({
  controllers: [GroupsController],
})
export class GroupsModule {}

My API could loop through that directory and try to import each module file.

  • pros:

    1. The custom code is kept away from the core repository
  • cons:

    1. NestJs uses Typescript so I have to compile the code first. How would I manage the API build and the builds from the custom apps? (Plug and play system)

    2. The custom extensions are very loose because they just contain some typescript files. Due to the fact they don’t have access to the node_modules directory of the API, my editor will show me errors because it can’t resolve external package dependencies.

    3. Some extensions might fetch data from another extension. Maybe the groups service needs to access the users service. Things might get tricky here.


Second approach: Keep each extension inside a subfolder of the src folder of the API. But add this subfolder to the .gitignore file. Now you can keep your extensions inside the API.

  • pros:

    1. Your editor is able to resolve the dependencies

    2. Before deploying your code you can run the build command and will have a single distribution

    3. You can access other services easily (/groups needs to find a user by id)

  • cons:

    1. When developing you have to copy your repository files inside that subfolder. After changing something you have to copy these files back and override your repository files with the updated ones.

Third approach:

Inside an external custom folder, all extensions are fully fledged standalone APIs. Your main API would just provide the authentication stuff and could act as a proxy to redirect the incoming requests to the target API.

  • pros:

    1. New extensions can be developed and tested easily
  • cons:

    1. Deployment will be tricky. You will have a main API and n extension APIs starting their own process and listening to a port.

    2. The proxy system could be tricky. If the client requests /users the proxy needs to know which extension API listens for that endpoint, calls that API and forwards that response back to the client.

    3. To protect the extension APIs (authentication is handled by the main API) the proxy needs to share a secret with those APIs. So the extension API will only pass incoming requests if that matching secret is provided from the proxy.


Fourth approach:

Microservices might help. I took a guide from here https://docs.nestjs.com/microservices/basics

I could have a microservice for the user management, group management etc. and consume those services by creating a small api / gateway / proxy that calls those microservices.

  • pros:

    1. New extensions can be developed and tested easily

    2. Separated concerns

  • cons:

    1. Deployment will be tricky. You will have a main API and n microservices starting their own process and listening to a port.

    2. It seems that I would have to create a new gateway api for each customer if I want to have it customizable. So instead of extending an application I would have to create a customized comsuming API each time. That wouldn’t solve the problem.

    3. To protect the extension APIs (authentication is handled by the main API) the proxy needs to share a secret with those APIs. So the extension API will only pass incoming requests if that matching secret is provided from the proxy.

Answer

There are several approaches to this. What you need to do is figure out what workflow is best suited for your team, organization, and clients.

If this was up to me, I would consider using one repository per module, and use a package manager like NPM with private or organization scoped packages to handle the configuration. Then set up build release pipelines that push to the package repo on new builds.

This way all you need is the main file, and a package manifest file per custom installation. You can independently develop and deploy new versions, and you can load new versions when you need to on the client-side.

For added smoothness, you could use a configuration file to map modules to routes and write a generic route generator script to do most of the bootstrapping.

Since a package can be anything, cross dependencies within the packages will work without much hassle. You just need to be disciplined when it comes to change and version management.

Read more about private packages here: Private Packages NPM

Now Private NPM registries cost money, but if that is an issue there are several other options as well. Please review this article for some alternatives – both free and paid.

Ways to have your private npm registry

Now if you want to roll your own manager, you could write a simple service locator, that takes in a configuration file containing the necessary information to pull the code from the repo, load it up, and then provide some sort of method to retrieve an instance to it.

I have written a simple reference implementation for such a system:

The framework: locomotion service locator

An example plugin checking for palindromes: locomotion plugin example

An application using the framework to locate plugins: locomotion app example

You can play around with this by getting it from npm using npm install -s locomotion you will need to specify a plugins.json file with the following schema:

{
    "path": "relative path where plugins should be stored",
    "plugins": [
        { 
           "module":"name of service", 
           "dir":"location within plugin folder",
           "source":"link to git repository"
        }
    ]
}

example:

{
    "path": "./plugins",
    "plugins": [
        {
            "module": "palindrome",
            "dir": "locomotion-plugin-example",
            "source": "https://github.com/drcircuit/locomotion-plugin-example.git"
        }
    ]
}

load it like this: const loco = require(“locomotion”);

It then returns a promise that will resolve the service locator object, which has the locator method to get a hold of your services:

loco.then((svc) => {
    let pal = svc.locate("palindrome"); //get the palindrome service
    if (pal) {
        console.log("Is: no X in Nixon! a palindrome? ", (pal.isPalindrome("no X in Nixon!")) ? "Yes" : "no"); // test if it works :)
    }
}).catch((err) => {
    console.error(err);
});

Please note that this is just a reference implementation, and is not robust enough for serious application. However, the pattern is still valid and shows the gist of writing this kind of framework.

Now, this would need to be extended with support for plugin configuration, initializations, error checking, maybe add support for dependency injection and so on.



Source: stackoverflow