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:
- Node.js >= v13 with ES6 module support
- Yarn (Workspaces)
- Lerna
- Microbundle
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
- Setup a Monorepo for Javascript backend services
- Add a Create React App frontend with a component libary
- Support Typescript in packages
- 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 :
1ekzemplo2 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 ekzemplo2cd ekzemplo3yarn init -yp4echo "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 -W2yarn 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
anduseWorkspaces
- remove attribute
workspaces
- lerna will use the definition frompackages.json
1{2 "version": "independent",3 "npmClient": "yarn",4 "useWorkspaces": true5}
Create the shared libary
each packages will need its own directory:
1mkdir -p packages/ekzemplo-common/src2cd packages/ekzemplo-common3yarn 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 to0.0.0
to allowlerna
to manage the version. - add
"source": "src/index.js",
to tellmicrobundle
about your entry file - change
"main": "dist/index.js",
todist/
folder - add
module
,exports
,unpkg
for modern formats (ESM bundle, ES2017) - add
"type":"module"
to switch to ES6 module support (=allowimport 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'23export const event = ()=>{4 return {5 id: faker.random.uuid(),6 name: faker.lorem.words(4),7 date: faker.date.future(),8 }9}1011export 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/src2cd services/ekzemplo-apievents3yarn init -y
add dependencies:
express
- https server libaryekzemplo-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 = 30105const apiPath = '/events'67app.get(apiPath, (req, res) => {8 let events = [];9 for (let index = 0; index < 10; index++) {10 const Event = appfaker.event()11 events.push(Event);1213 }14 res.send(JSON.stringify(events))15})1617app.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/src2cd services/ekzemplo-apieusers3yarn init -y
add dependencies:
express
- https server libaryekzemplo-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'34const app = express()5const port = 30116const apiPath= '/users'78app.get(apiPath, (req, res) => {9 let users = [];10 for (let index = 0; index < 10; index++) {11 const User = user()12 users.push(User);1314 }15 res.send(JSON.stringify(users))16})1718app.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