Monorepo Setup for Javascript, Typescript and React with NPM packages (1/4)

Executive brief

Setup an environment with independed npm packages for Javascript, Typescript and React components or libs which support all modern package formats.

Tooling

These are the tools we will use under the hood:

Goals

  • reuse codeparts, react components and type definition between frontend and backend
  • reuse codeparts, react components and type definition in other project as npm packages
  • support mixed Javascript and Typescript development
  • can be used for fullstack projects (frontend, api, libaries)

Sections

  1. Setup a Monorepo for Javascript backend services
  2. Add a Create React App frontend with a component libary
  3. Support Typescript in packages
  4. Deploy to private Github NPM registry

Example "ekzemplo" App Use Case

  • As a User I can lists public events
  • As a publisher I can login to the backend
  • As a publisher I can create a new event

Architecture

  • Website (React App)
  • Administration Backend (React App)
  • manage events api (Microservice)
  • manage backend users api (Microservice)
  • shared libary (libary)

this is the proposed directory structure :

1ekzemplo
2 services/
3 ekzemplo-web/
4 ekzemplo-admin/
5 ekzemplo-apievents/
6 ekzemplo-apiusers/
7 packages/
8 ekzemplo-common/
9 ekzemplo-components/


1) Setup a Monorepo for Javascript backend service

Goals

share a utility libary between multiple microservices

Setup the repository

we need to create the repo root:

1mkdir ekzemplo
2cd ekzemplo
3yarn init -yp
4echo "node_modules" > .gitignore

Hint: '-y': skip interactice session, '-p': add private: true to the `package.json``

Open package.json in your editor:

  • remove the attribute main
  • set version to 0.0.0
  • add workspaces
1{
2 "name": "ekzemplo",
3 "version": "0.0.0",
4 "license": "MIT",
5 "private": true,
6 "workspaces": [
7 "packages/*"
8 ]
9}

Add lerna and microbundl as workspace dependencies -W and initalize lerna:

1yarn add lerna microbundle --dev -W
2yarn lerna init --independent

Lerna init creates the packages/ folder and a lerna.json file. The flag --independent sets the version filed in lerna.json to version each package independed.

Tipp (MacOS only) exclude node_modules from TM backup tmutil addexclusion $(pwd)/node_modules

Next, modify lerna.json to integrate yarn:

  • add attributes npmClient and useWorkspaces
  • remove attribute workspaces - lerna will use the definition from packages.json
1{
2 "version": "independent",
3 "npmClient": "yarn",
4 "useWorkspaces": true
5}


Create the shared libary

each packages will need its own directory:

1mkdir -p packages/ekzemplo-common/src
2cd packages/ekzemplo-common
3yarn init -y

to create a bundle which supports old and new bundle formats we change package.json to use microbundle in

  • change the attribute version in package.json to 0.0.0 to allow lerna to manage the version.
  • add "source": "src/index.js", to tell microbundle about your entry file
  • change "main": "dist/index.js", to dist/ folder
  • add module, exports, unpkg for modern formats (ESM bundle, ES2017)
  • add "type":"module" to switch to ES6 module support (=allow import from statements)
  • add scripts to rebuild on changes during development
1{
2 "name": "ekzemplo-common",
3 "version": "0.0.0",
4 "source": "/src/index.js",
5 "main": "dist/index.cjs",
6 "module": "dist/index.module.mjs",
7 "unpkg": "dist/index.umd.js",
8 "type":"module",
9 "license": "MIT",
10 "scripts": {
11 "start": "microbundle watch",
12 "build": "microbundle build"
13 }
14}

The shared libary will provide a function to validate create fake users and events based on faker:

1yarn add faker

then add src/appfakes.js

1import faker from 'faker'
2
3export const event = ()=>{
4 return {
5 id: faker.random.uuid(),
6 name: faker.lorem.words(4),
7 date: faker.date.future(),
8 }
9}
10
11export const user = ()=>{
12 return {
13 id: faker.random.uuid(),
14 username: faker.internet.userName(),
15 password: 'pw',
16 }
17}

and src/index.js

1export * from './appfakes'

build the package in dist/:

1yarn build


create microservice events

1mkdir -p services/ekzemplo-apievents/src
2cd services/ekzemplo-apievents
3yarn init -y

add dependencies:

  • express - https server libary
  • ekzemplo-common - local shared libary
1yarn add express ekzemplo-common -W

Hint: do not forget -W to inform yarn to link the existing package from your monorepo

create a microservice which uses the CommonJS Module Syntax:

1const express = require('express')
2const appfaker = require('ekzemplo-common')
3const app = express()
4const port = 3010
5const apiPath = '/events'
6
7app.get(apiPath, (req, res) => {
8 let events = [];
9 for (let index = 0; index < 10; index++) {
10 const Event = appfaker.event()
11 events.push(Event);
12
13 }
14 res.send(JSON.stringify(events))
15})
16
17app.listen(port, () => {
18 console.log(`microsoervice events listening at http://localhost:${port}${apiPath}`)
19})

add a start command to package.json:

1{
2 "name": "ekzemplo-apievents",
3 "version": "1.0.0",
4 "main": "./src/server.js",
5 "license": "MIT",
6 "dependencies": {
7 "ekzemplo-common": "^0.0.1",
8 "express": "^4.17.1"
9 },
10 "scripts":{
11 "start":"node src/server.js"
12 }
13}

test the microservice:

1yarn start

request the events from the api in your browser: http://localhost:3011/events

you should get as a result

1[
2 {
3 "id": "aa4f1908-7805-41a3-a110-2440126eff60",
4 "name": "doloremque natus eius beatae",
5 "date": "2021-12-04T04:45:13.312Z"
6 },
7 {
8 "id": "f3fc0b0e-b448-4a28-92fa-fc539973d501",
9 "name": "nihil omnis tenetur qui",
10 "date": "2021-05-28T07:39:17.802Z"
11 }
12]


create microservice users (ES6 module)

1mkdir -p services/ekzemplo-apieusers/src
2cd services/ekzemplo-apieusers
3yarn init -y

add dependencies:

  • express - https server libary
  • ekzemplo-common - local shared libary
1yarn add express ekzemplo-common -W

Hint: do not forget -W to inform yarn to link the existing package from your monorepo

create a microservice which uses the ES6 Module Syntax:

1import express from 'express'
2import {user} from 'ekzemplo-common'
3
4const app = express()
5const port = 3011
6const apiPath= '/users'
7
8app.get(apiPath, (req, res) => {
9 let users = [];
10 for (let index = 0; index < 10; index++) {
11 const User = user()
12 users.push(User);
13
14 }
15 res.send(JSON.stringify(users))
16})
17
18app.listen(port, () => {
19 console.log(`microsoervice users listening at http://localhost:${port}${apiPath}`)
20})

add to package.json:

  • add "type":"module", for the ES6 module syntax
  • a start command
1{
2 "name": "ekzemplo-apiusers",
3 "version": "1.0.0",
4 "main": "./src/server.js",
5 "type":"module",
6 "license": "MIT",
7 "dependencies": {
8 "ekzemplo-common": "^0.0.1",
9 "express": "^4.17.1"
10 },
11 "scripts":{
12 "start":"node src/server.js"
13 }
14}

test the microservice:

1yarn start

request the users from the api in your browser: http://localhost:3011/users

you should get as a result

1[
2 {
3 "id": "fc6af2ae-6457-4de6-8573-0e86ab609a15",
4 "username": "Laurianne.Mertz37",
5 "password": "pw"
6 },
7 {
8 "id": "f331f161-27f0-4f53-aa46-b828573ac88b",
9 "username": "Yasmeen.Frami88",
10 "password": "pw"
11 }
12]


Run all Services with one command

add this to the root package.json:

1{
2 "name": "ekzemplo",
3 "version": "0.0.0",
4 "license": "MIT",
5 "main": "index.js",
6 "author": "Andreas Heissenberger <andreas@heissenberger.at>",
7 "private": true,
8 "workspaces": [
9 "packages/*"
10 ],
11 "devDependencies": {
12 "lerna": "^3.22.1",
13 "microbundle": "https://github.com/aheissenberger/microbundle.git"
14 },
15 "scripts": {
16 "start": "lerna run start --parallel"
17 }
18}

test it with yarn start



next part: Add a Create React App frontend with a component libary

Write a response on Medium