Typescript Microservice Project: From 0 to 1

Shihao Xia
5 min readMar 11, 2020
Copyright: n-ix.com by Romana Gnatyk

This article mainly focus on providing a methodology to elegantly construct a large typescript project in order to manage/control dependencies, share/test code and deploy executable.

Use Monorepo Not “Multirepo”

Centralized Develop, distributed deploy.

Monorepo is not monolith. Monorepo means the code is kept in the same repository if your CVS is Git. Unlike “Multirepo”, each service is stored by individual git repository,The code is stored as a whole logical object in a single git repository.

Why? It reduces the complexity and difficulty for sharing the code among services. It also infers that you should reuse your code as much as possible.

Why? Better dependency control. For Node.js based project, package.json(NPM) is an inevitable topic. Imaging the pain for updating a same dependency version in multiple repositories for multiple projects, Monorepo can make dependency control become one click operation, with the help of lerna(A tool for managing JavaScript projects with multiple packages).

Why? Accelerating development speed. Monorepo frees you from updating/publishing/syncing package version for the other packages when you done some changes in a package. “Paths” functionality from tsconfig.json makes sharing code/type cannot be easier. We will discuss it later.

Although there are many ways to construct your project skeleton, which are hard to determine which one is better than the others, I still want to share my setup cause it really helps me saving lots of time and benefits from developing speed and quality, with scalability and elegance.

Project Structure

Essential concepts:

  • Concept 1: Source code has only two types: executable and non-executable

Executable: In microservice project world, it means “service”, whose purpose is to be executed.

Non-executable: The purpose is to provide utility tools (functions, classes, etc..) used in executable code unit, such as libraries, helpers, shared type interface, etc..

  • Concept 2:Separating source code and configuration

Configuration contains files like tsconfig.json, package.json, envs, yamls. These files better to be stored separately from source code. It will keep your large project clean and readable.

I will explain these two concepts more at below.

Here is an example of a typescript microservice project:

Example of a typescript microservice project

Root folders can be roughly divided into two part: code related and project related, inspired by concept 2.

Code related:

src, tests, resources, configs. They have exact same child structure: each service and lib (inspired by concept 1) will have a folder under them. In another word, services and libs are composed by src, tests, resources and configs.

Project related:

Root tsconfig.json: Typescript configurations that shared by all code unit, usually will be some code standards like target (es5, es2018, etc..), paths, src and dist.

Root package.json: Dependencies and devDependencies shared by whole project, like typescript, lerna, ts-node, tsconfig-paths, eslint, etc..

Other folders like dockerfiles, scripts are fairly straightforward.

Sharing Code

Power of paths from tsconfig.json

Paths settings enable you to define custom path mapping between your import path and physical file path.

Paths in tsconfig.json

Why? Getting rid of “../../../../” in import path makes your code clean and organizable.

Why? Decoupling the file logic structure and physical structure reduces your cost when moving files.

With the help of tsconfig-paths, you can import code like

Import code by using custom mapping paths

If an operation is repeatable(periodic) and used in scale, it should be managed through centralizing or automating.

Manage & Install Dependency

Power of lerna

With the help of lerna, we can ensure there is only one entity of dependency exists when installing whole dependencies. Other functionality like lerna add, lerna version are also very useful.

Why? One dependency can be needed in multiple package.json files, for example express will be used in every service package. Installing same dependency multiple times is not a good idea as the number of project growing. Lerna can extract same dependencies and install in the root node_modules folder.

Why? Saving disk space.

Why? Increasing installing speed.

Why? Centralized control of package.json (Install all dependencies of all package.json).

Power of syncpack

Consistent dependency version management in microservice is critical for service quality and security. So you can have one command in npm scripts to sync dependency version.

Why? It ensures all package.json have same dependency version to improve entirely security and integrity.

Deploy & Test

Base Docker Image

It is quite useful that install all same dependencies that will used in multiple services into a base image.

Why? Avoid install & clean same dependencies again and again in docker build.

Why? Increasing docker build speed.

Why? Decoupling dependencies with code, cleaner, faster and more stable.

Why? Saving network traffic, disk space no matter in local debugging & testing environment or ci/cd pipeline environment.

Single Docker Image for All Executables (Service)

With the power of “ARG” in docker image, using one docker image for all deployable target is possible.

Example of universal dockerfile for all services

Thanks for the concept 1 and concept 2, we can easily come up with a universal docker file. Only difference among all services is just service name. We just need to copy corresponding files with given service name. Now you can deploy your numerous services with only single dockerfile.

Why? Sharing docker file can reduce complexity and cost dramatically. You only need to maintain one dockerfile no matter how many deployable targets you have.

Why? Security and integrity: Update and upgrade only need to change at one place. All deployable targets will be affected.

Why? Provides universal way to run tests and build ci/cd pipeline for any deployable target.

Same strategy can be used for testing too.

This article only describes some high level ideas, might be beneficial in a typescript microservice project.

Please comment let me know what do you think. Welcome to any advices.

--

--