This projects aims to provide an opinionated template structure for typescript monorepo with an API example structure.
The following softwares need to be install:
- Bun - all-in-one JavaScript runtime & toolkit designed for speed, complete with a bundler, test runner, and Node.js-compatible package manager.
- Docker - software platform that lets you rapidly build, test and deploy applications using containers.
- Helm - package manager for Kubernetes.
- Kind - local Kubernetes clusters through Docker.
- kubectl - command-line tool to deploy and manage applications in Kubernetes.
The following tools are provided with the template:
- Commitlint - commit message linter.
- Eslint - javascript linter.
- Husky - git hooks wrapper.
- Proto - javascript toolchain.
- Turbo - repo building system with pipeline management.
This model also includes recommendations for vscode settings and extensions (see .vscode/settings.json and .vscode/extensions.json).
To get a better developer experience, install globally Ni, a Nodejs package manager wrapper:
bun install --global @antfu/ni
The API example is built on top of Fastify, a powerful api framework that handles hooks and provides numerous plugins, including the following already in use:
The API is fully typed and controlled over Zod schemas to improve data validation in such backends or frontends (thanks to the shared package that handle schemas and can be imported by any other apps or packages).
In addition, the template uses TS-Rest a fully typed RPC client for REST APIs that comes with plugins to automatically generate OpenAPI schemas and integrate with Fastify:
Notes:
- Swagger UI is available at
http(s)://<api_domain>/swagger-ui
.- A function
getApiClient
that returns an apiClient (using fetch, but could be extended to use axios or others) is exported from theshared package
, it is useful for other apps / packages that needs to consume the API.
A configuration management system enables type checking and automatic replacement of values in the following order default variables => configuration file variables => environment variables
.
The environment variables are parsed to extract only the keys with the given prefixes (default parsed prefix set here) to improve security, and the keys are divided by the __
(double underscore) identifier to recreate the configuration object (note that the array must be passed as JSON in the environment variables).
Prisma is used as an example ORM in the template, providing complete and simplified control of the database based on API code (migrations, simplified queries, etc.). The code base has been split as much as possible to allow easy migration to other ORMs such as Drizzle, Mongoose or others; to do this, simply replace the prisma/
folder with the corresponding solution and update the resources/**/queries.ts
files.
The packages
folder can be used to share resources between different applications, and is already used to share eslint
/ typescript
configurations, a test-utils
utility package for testing and a shared
package containing Zod schemas and API contracts. It can also be used to share utility functions, schemas and so on between different applications or packages.
Unit tests are run using Vitest, which is compatible with the Jest api but is faster when working on top of Vite.
End to end and component tests are powered by Cypress and could be managed in the ./packages/cypress
folder.
Notes: Test execution may require some packages to be built, and the pipeline dependencies are described in the
turbo.json
file.
Documentation is ready to write in the ./apps/docs
folder, it uses Vitepress, a static website generator using Vite and Vue that will parse .md
files to generate the documentation website.
Default Github Actions workflows are already set to run some automated checks over 2 main files, the first one ci.yml run on pull request with the following tasks :
Description | File |
---|---|
Lint | lint.yml |
Unit tests - (With optional code quality scan) [1] | tests-unit.yml |
Build application images [2] | build.yml |
End to end tests OR Deployment tests [3] | tests-e2e.yml / tests-deploy.yml |
Vulnerability scan [4] | scan.yml |
Notes:
[1] Run code quality analysis using Sonarqube scanner, it only run if the secrets
SONAR_HOST_URL
,SONAR_TOKEN
,SONAR_PROJECT_KEY
are set in the repositry interface.[2] Build application images and tag them
pr-<pr_number>
before pushing them to a registry.[3] Run e2e tests if changes occurs on apps, dependencies or helm / Run deployment tests if changes don't occurs in apps, dependencies or helm.
[4] Run only if changes occurs in
apps
,packages
or.github
folders and base branch ismain
.
The second file cd.yml is responsible to publish new release using Release-please-action that automatically parse git history following Conventionnal Commit to build changelog and version number (see. Semantic versioning). It can be triggered manually to run the following tasks :
Description | File |
---|---|
Create new release pull request / Create new git tag and github release | release.yml |
Build application images and push them to a registry | build.yml |
Notes: Uncomment on push trigger in
cd.yml
file to automatically create the new PR on merge into the main branch.
All docker images are built in parallel using the matrix.json file, some options are available to build multi-arch with or whithout QEMU (see. build.yml).
In addition, this template uses cache for Bun, Turbo and docker to improve CI/CD speed when it is possible. The cache is deleted when the associated pull request is closed or merged (see. cache.yml).
Application preview can be enabled using the Argo-cd PR generator, whenever a pull request is tagged with the preview
label, a preview of the application's current state (using images tagged pr-<pr_number>
) will be deployed in a Kubernetes cluster.
To activate this feature, you need to :
- Create a Github App to ensure Argo-cd will access to the repository and receive webhooks.
- Deploy an ApplicationSet based on this template.
- Create Github Actions environment variables templates. Following the base template you should create 2 variables, one called
API_TEMPLATE_URL
with the valuehttps://api.pr-<pr_number>.domain.com
and the other calledDOCS_TEMPLATE_URL
with the valuehttps://docs.pr-<pr_number>.domain.com
.
An example of a Helm structure is provided in the ./helm
folder to facilitate deployment in a Kubernetes cluster.
This type of structure makes it easy to add another service with little effort by adding a new service folder in ./helm/templates
, add helpers functions in _helpers.tpl and add a service block in values.yaml. Example :
- Copy
./helm/templates/api
folder to./helm/templates/<service_name>
.cp -R ./helm/templates/api ./helm/templates/<service_name>
- Inside the newly created files
./helm/templates/<service_name>/*
, replace all.Values.api
with.Values.<service_name>
andtemplate.api
withtemplate.<service_name>
.find ./helm/templates/<service_name> -type f -exec perl -pi -e 's|\.Values\.api|\.Values\.<service_name>|g' {} \; find ./helm/templates/<service_name> -type f -exec perl -pi -e 's|\.template\.api|\.template\.<service_name>|g' {} \;
- Copy - paste all
template.api.*
functions in./helm/templates/_helpers.tpl
and rename them totemplate.<service_name>
. - Copy - paste the
api
block in./helm/values.yaml
and rename it to<service_name>
.
Another improvement that should be made is to put the ./helm
directory in a dedicated repository so that it can be used as a Helm registry with version control, see :
- https://helm.sh/docs/topics/chart_repository#github-pages-example
- https://helm.sh/docs/howto/chart_releaser_action
- https://github.com/this-is-tobi/helm-charts
Github templates are already define with a base structure that just need to be updated, see :
Structure used for typescript applications :
./
├── apps
│ ├── api
│ └── docs
├── packages
│ ├── cypress
│ ├── shared
│ ├── test-utils
│ └── ts-config
├── bun.lockb
└── package.json
Structure used in the API example :
./apps/api
├── src
│ ├── prisma
│ ├── resources
│ │ ├── system
│ │ │ ├── index.ts
│ │ │ └── router.ts
│ │ └── users
│ │ ├── business.ts
│ │ ├── index.ts
│ │ ├── queries.ts
│ │ └── router.ts
│ ├── utils
│ ├── app.ts
│ ├── database.ts
│ └── server.ts
├── Dockerfile
├── package.json
├── tsconfig.json
├── vite.config.ts
└── vitest.config.ts
Structure used for helm deployment :
./helm
├── charts
├── templates
│ ├── api
│ │ ├── configmap.yaml
│ │ ├── deployment.yaml
│ │ ├── hpa.yaml
│ │ ├── ingress.yaml
│ │ ├── pullsecret.yml
│ │ ├── secret.yaml
│ │ ├── service.yaml
│ │ └── serviceaccount.yaml
│ ├── docs
│ │ ├── configmap.yaml
│ │ ├── deployment.yaml
│ │ ├── hpa.yaml
│ │ ├── ingress.yaml
│ │ ├── pullsecret.yml
│ │ ├── secret.yaml
│ │ ├── service.yaml
│ │ └── serviceaccount.yaml
│ └── _helpers.tpl
├── Chart.yaml
└── values.yaml
# Clone this template
bunx degit https://github.com/this-is-tobi/template-monorepo-ts <project_name>
# Go to project directory
cd <project_name>
# Init git on the new project
git init
# Init example files
sh ./ci/scripts/init-env.sh
# Install dependencies
bun install
# Build packages
bun run build
A bunch of commands are available through package.json scripts.
Local :
# Start development mode
bun run dev
# Lint the code
bun run lint
# Format the code
bun run format
# Run unit tests
bun run test
# Run unit tests with coverage
bun run test:cov
# Run end to end tests - this requires `bun run dev` to be run in another terminal
bun run test:e2e
# Run end to end tests (CI mode) - this requires `bun run dev` to be run in another terminal
bun run test:e2e-ci
Docker :
# Start development mode in docker
bun run docker:dev
# Start production mode in docker
bun run docker:prod
# Run end to end tests in docker
bun run docker:e2e
# Run end to end tests in docker (CI mode)
bun run docker:e2e-ci
Kubernetes :
# Setup prerequisite for kubernetes
bun run kube:init
# Start development mode in kubernetes
bun run kube:dev
# Start production mode in kubernetes
bun run kube:prod
# Remove app resources in kubernetes
bun run kube:clean
# Delete kubernetes cluster
bun run kube:delete
# Run end to end tests in kubernetes
bun run kube:e2e
# Run end to end tests in kubernetes (CI mode)
bun run kube:e2e-ci
Notes: Bun command can be used with filter flag to trigger a script in a given package.json (ex:
bun run --cwd <package_path> <script_name>
).
Application | URL (local / docker) | URL (kubernetes) |
---|---|---|
API | http://localhost:8081 | http://api.domain.local |
API - swagger | http://localhost:8081/swagger-ui | http://api.domain.local/swagger-ui |
Documentation | http://localhost:8082 | http://doc.domain.local |
Notes: If the containers are healthy but the services are not resolved with Kubernetes, check that the domains are mapped to
127.0.0.1
in/etc/hosts
, which is what Bun should do by running thekube:init
command..