Monorepos : Building with nx: An ExpressJS and NextJS Application
What are Monorepos?
The textbook definition of a monorepo is “A monorepo is a single repository containing multiple distinct projects, with well-defined relationships.” Which sounds an awful lot like the monolithic architecture, which is really hated in the world of “microservices”. Nothing against monoliths considering Instagram is one of the largest monoliths out there. But, we have seen the cons of using a monolithic; but wait
✋ Monorepo ≠ Monolith
A good monorepo is the opposite of monolithic! Read more about this and other misconceptions in the article on “Misconceptions about Monorepos: Monorepo != Monolith”.
TLDR:
- Everything at that current commit works together.
- Changes can be verified across all affected parts of the organization.
- Easy to split code into composable modules
- Easier dependency management
- One toolchain setup
- Code editors and IDEs are “workspace” aware
- Consistent developer experience
Is Google still using monorepo?
Believe it or not, Google is one of the biggest monorepo users in our industry. I’m not making this up, their code base is huge, as you can probably imagine, and reports state that they have 95% of it inside the same repository. I rest my case 🎯
In all seriousness, I’m just documenting my learning for a project at work. Anyways #LearningInPublic.
Creating a simple api to Fetch Cricket Players 🏏
Let’s start with creating a simple express app and make a REST controller that fetches the data from the our dataset.
Let’s initialize a repository. Initialize a repository using:
npx create-nx-workspace — preset=express
Opens an interactive shell and initializes the repo; repository name = nx-cricket; application name = nx-cricket-api (if you wish to follow to the t)
nx serve nx-cricket-api # to test the app
I’ve downloaded the dataset https://data.world/raghav333/cricket-players-espn and cleaned the dataset and wrote a simple python script to convert it into a JSON to make it easier export it as a typescript array. Note: the entire Code/Dataset is available on GitHub.
export interface Cricketer {
Id: string
Name: string;
Country: string;
'Full name': string;
Age: string;
'Major teams': string;
'Batting style': string;
'Bowling style': string;
Other: string;
}
export const cricketers: Cricketer[] = [
{
"Id": "0",
"Name": "Henry Arkell",
"Country": "England",
"Full name": "Henry John Denham Arkell",
"Age": "84",
"Major teams": "Northamptonshire",
"Batting style": "Right-hand bat",
"Bowling style": "",
"Other": ""
}, ...
]
Define a Simple Controller in nx-cricket-api>src>main.ts
Lets just define two simple controllers to retrieve all the cricketers and Query endpoint to return cricketer by name.
// Returns all the cricketers
app.get('/cricketers', (_, res) => {
res.send({cricketers});
});
// Returns cricketer: Lazy Search by Name
app.get('/search', (req, res) => {
const q = ((req.query.q as string) ?? '').toLocaleLowerCase();
res.send(cricketers.filter( ({ Name }) =>
Name.toLocaleLowerCase().includes(q)
));
});
Let’s create a simple frontend with Next.js
First we have to install the nrwl/next if it has not already been installed. Then we create a simple next app, a next app boilerplate is generated. We add a simple application that renders the list of cricket players
# Installing nrwl/next
# yarn
yarn add --dev @nrwl/next
# npm
npm install --save-dev @nrwl/next
# create nx app
nx g @nrwl/next:app
name = nx-cricket-search
style = css
Removed all the boilerplate and added this basic frontend code
import { useEffect, useState, useCallback } from 'react';
import React from 'react';
import { Cricketer } from '@nx-cricket/shared-types';
export function Index() {
const [search, setSearch] = useState('');
const [cricketer, setCricketer] = useState<Cricketer[]>([]);
useEffect(() => {
fetch(`http://localhost:3333/search?q=${escape(search)}`)
.then((resp) => resp.json())
.then((data) => setCricketer(data));
}, [search]);
const onSetSearch = useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) => {
setSearch(evt.target.value);
},
[]
);
return (
<div>
<input
style={{ padding: '10px', margin: '20px' }}
value={search}
placeholder="Enter Cricketer Name"
onChange={onSetSearch}
/>
<ul>
{cricketer.map(({ Id, Name, Country, Age }) => (
<li key={Id}>
{Name}, {Country}, {Age}
</li>
))}
</ul>
</div>
);
}
export default Index;
You can also add server-side rendering. But for this tutorial I’ve decided to keep it simple.
Shared types
One of the best features of monorepos is the ability to use shared types, if you see the frontend code, we have strongly typed the cricketer array, this was because we were able to add the type ~ interface in shared-types folder from the Cricketer.ts file. This is one of the most useful features of the monorepo structure.
# Creating a shared Library
nx g @nrwl/node shared-types
# In libs>shared-types>src>index.ts add | already found in our Cricketer.tsc
export interface Cricketer {
Id: string
Name: string;
Country: string;
'Full name': string;
Age: string;
'Major teams': string;
'Batting style': string;
'Bowling style': string;
Other: string;
}
# Cleanup cricketer.ts
import type { Cricketer } from "@nx-cricket/shared-types"
export const cricketers: Cricketer[] = [
{
"Id": "0",
"Name": "Henry Arkell",
"Country": "England",
"Full name": "Henry John Denham Arkell",
"Age": "84",
"Major teams": "Northamptonshire",
"Batting style": "Right-hand bat",
"Bowling style": "",
"Other": ""
},...
]
# type { Cricketer } from "@nx-cricket/shared-types" can be accessed from any project