Sharing TypeScript Code Between Microservices: A Guide Using Git Submodules

Published: April 21, 2023

In a microservices architecture, sharing code among different services is a common challenge that developers face. Often, there are utility functions or modules that are useful across multiple microservices, and duplicating this code in each project leads to maintenance overhead and potential inconsistencies. In this article, we'll explore how to efficiently share TypeScript code between microservices using Git submodules.

Introducing Git Submodules

Git submodules allow us to include one Git repository as a subdirectory within another repository. By leveraging this feature, we can create a separate repository for shared code and have microservice repositories include it as a submodule. This approach has several advantages:

  1. Modularity: Each microservice can control when to update to a new version of the shared code by pointing to a specific commit of the submodule.

  2. Separate Repository for Shared Code: The shared code repository doesn't need to have its own package.json. It can solely consist of TypeScript files, making it lightweight and dedicated to utility functions.

Step-by-Step Guide

  1. Create a Shared Code Repository: Start by creating a new Git repository specifically for the shared code. For instance, you could name it utils-{shared code name}.

  2. Create Microservice Repositories: Next, create separate repositories for each microservice you want to develop. For example, you might have {microservice name} for one of the microservices.

  3. Add Git Submodule to Microservice Repository: In the microservice repository, create a utils folder where we'll include the shared code as a submodule.

    • Use the following command to add the Git submodule: git submodule add git@github.com:xxx ./src/utils/kibana-logger-ts
    • This command links the kibana-logger-ts submodule to the ``./src/utils/kibana-logger-ts` directory within the microservice repository.
  4. In my code at least, I configure ts-node for development in the tsconfig.json:

    • Run npm i -D tsconfig-paths
    • Add the following to the tsconfig.json:
tsconfig.json
    "ts-node": {
        "require": ["tsconfig-paths/register"]
    },
  1. Path Mapping in tsconfig.json: Modify the tsconfig.json of the microservice to include path mappings alias' for the shared code. Rename the import as to how we want to use the util.
    • This makes the code from the src folder of the microservice repository access any submodule package easily.
tsconfig.json
{
  "compilerOptions": {
    ...
    "baseUrl": "src",
    ...
    "paths": {
      "@kibana-logger": ["utils/kibana-logger-ts/src"],
    }
  },
  ...
}

Now we will import the shared code like:

import { KibanaFactory } from "@kibana-logger";

Another option here is to create an alias for the entire utils folder and import what we need from there:

tsconfig.json
{
  "@utils/*": ["utils/*"]
}

Here the imports will be like:

import { KibanaFactory } from "@utils/kibana-logger-ts/src";

With this option, we will not need to manually update the tsconfig.json every time we add a new submodule.

  1. Handling Production Build with Babel The above works in dev. From the docs, note that this Path-mapping feature does not change how import paths are emitted by tsc, so paths should only be used to inform TypeScript that another tool has this mapping and will use it at runtime or when bundling. For prod, we need to add these aliases into the built JavaScript code.

We could run the microservice with ts-node in prod, but this is not recommended:

{
  ...
  "start": "npm run build &&  node -r ts-node/register/transpile-only -r tsconfig-paths/register build/index.js",
  ...
}

I have chosen instead to use Babel to transpile the code.

  • Install needed packages for dev: npm i -D @babel/cli babel-plugin-module-resolver
  • Created a .babelrc in the root of your project:
.babelrc
{
  "compact": false,
  "retainLines": true,
  "minified": false,
  "inputSourceMap": false,
  "sourceMaps": false,
  "plugins": [
    [
      "module-resolver",
      {
        "alias": {
          "@kibana-logger": "/utils/kibana-logger-ts/src"
        }
      }
    ]
  ]
}

Also, add this to your Dockerfile to copy the .babelrc file during the initial build:

COPY .babelrc ./

Now, your microservice can be built and run with Babel in the production environment; it will work in the container built by docker.

  1. Making changes to the shared code: When it's time to make changes to the shared code, you have two options:
    • Option 1:
      • Clone the shared util code separately, make the necessary changes, and push them up. Then in the microservice repository, run git submodule update --remote to update the submodule to the latest commit.
    • Option 2:
      • modify the Submodule directly into the microservice. Then, push changes from SubModule, and the changes will be available to everyone using the shared-code repository.
  2. Managing External Dependencies: to handle external dependencies in the shared code, we have two options:
    • Option 1:
      • Install the dependencies in the shared code repository, and then add the dependencies to the microservice repository.
      • perform npm install in the shared code repository.
      • Then running npm start in the microservice works fine as it can compile and finds the node_modules directory of the submodule
⚠️

There are some scenarios, e.g.graphql lib, that has a runtime check to ensure a unique dependency on the library. Relying on the two node_modules causes a runtime (not compilation time) error. Hence, solution 2 might be the only possible one here

  • Option 2:
    • Install Child Dependencies into Parent Dependencies.
    • can use the npm install with file: to add the dependencies manually.
    • npm install --save file:utils/utils-{shared code name}
    • Most of our repositories have about the same dependencies. So it is manageable to rely on manually taking the dependencies.

I personally use option 2.

Additional Notes

Here are some additional notes and tips to keep in mind when working with Git submodules:

  • Initializing Submodules: When cloning a project from Git for the first time, the submodules are not automatically initialized. To fetch the submodule's Git repositories, run the command git submodule update --init at the root of the microservice project.
  • .gitmodules File: When you perform the git submodule command, it creates a hidden file called .gitmodules within the parent repository. This file keeps track of the submodule's URL and path.
  • Committing Changes in Submodules: Remember that performing git add . at the root of the microservice project only adds the files from the parent. However git status at the root of the parent repository shows the changes with the submodule changes. To commit and push changes for the submodule, you need to navigate to the submodule folder and perform the git commands

resources: