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:
-
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.
-
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
-
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}
. -
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. -
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.
- Use the following command to add the Git submodule:
-
In my code at least, I configure
ts-node
for development in thetsconfig.json
:- Run
npm i -D tsconfig-paths
- Add the following to the
tsconfig.json
:
- Run
"ts-node": {
"require": ["tsconfig-paths/register"]
},
- 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.
{
"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:
{
"@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.
- 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:
{
"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.
- 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.
- Clone the shared util code separately, make the necessary changes, and push them up. Then in the microservice repository, run
- 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.
- Option 1:
- 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
- Option 1:
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
withfile:
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. Howevergit 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: