Monorepo Development With React, Node.js, and PostgreSQL With Prisma and ClickHouse
Simplify full-stack development by using a monorepo to house your React frontend, Node.js backend, and PostgreSQL database, all accessed through Prisma.
Join the DZone community and get the full member experience.
Join For FreeWhat's the Big Idea?
Building web apps with separate frontends, backends, and databases can be a headache. A monorepo puts everything in one place, making it easier to share code, develop locally, and test the whole app together. We showed how to build a simple signup dashboard using React, Node.js, PostgreSQL (with Prisma for easy database access), and optionally ClickHouse for fast data analysis, all within a monorepo structure. This setup helps you scale your app cleanly and makes life easier for your team.
In this guide, we're going to build a super simple app that shows how many people sign up each day. We'll use:
- React: To make the pretty stuff you see on the screen.
- Node.js with Express: To handle the behind-the-scenes work and talk to the database.
- PostgreSQL: Our main place to store important info.
- Prisma: A clever tool that makes talking to PostgreSQL super easy and helps avoid mistakes.
- ClickHouse (optional): A really fast way to look at lots of data later on.
- All living together in one monorepo!
Why Put Everything Together?
- Everything in one spot: Issues, updates, and reviews all happen in the same place.
- Easy peasy development: One setup for everything! Shared settings and simple ways to start the app.
- Sharing is caring (Code!): Easily reuse little bits of code across the whole app.
- Testing the whole thing: You can test how the frontend, backend, and database work together, all at once.
- Grows with you: Adding new parts to your app later is a breeze.
What We're Making: A Signup Tracker
Imagine a simple page that shows how many people signed up each day using a chart:
- Frontend (React): The part you see. It'll have a chart (using Chart.js) that gets the signup numbers and shows them.
- Backend (Express): The brain. It will grab the signup numbers from the database.
- Database (PostgreSQL): Where we keep the list of who signed up and when. We can also use ClickHouse later to look at this data in cool ways.
- Prisma: Our friendly helper that talks to PostgreSQL for us.
How It's All Organized
visualization-monorepo/
│
├── apps/
│ ├── frontend/ # The React stuff
│ └── backend/ # The Node.js + Express + Prisma stuff
│
├── packages/
│ └── shared/ # Bits of code we use in both the frontend and backend
│
├── db/
│ └── schema.sql # Instructions for setting up our PostgreSQL database and adding some initial info
│
├── docker-compose.yml # Tells your computer how to run all the different parts together
├── .env # Where we keep secret info, like database passwords
├── package.json # Keeps track of all the tools our project needs
└── README.md # A file explaining what this project is all about
Setting Up the Database (PostgreSQL)
Here's the basic structure for our db/schema.sql file:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL,
date DATE NOT NULL
);
-- Let's add some fake signup data
INSERT INTO users (email, date) VALUES
('[email protected]', '2024-04-01'),
('[email protected]', '2024-04-01'),
('[email protected]', '2024-04-02');
This code creates a table called users with columns for a unique ID, email address, and the date they signed up. We also put in a few example signups.
Getting Prisma Ready (Backend)
This is our apps/backend/prisma/schema.prisma file:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String
date DateTime
}
This tells Prisma we're using PostgreSQL and where to find it. The User model describes what our users table looks like.
Here's how we can use Prisma to get the signup counts:
const signups = await prisma.user.groupBy({
by: ['date'],
_count: true,
orderBy: { date: 'asc' },
});
This code asks Prisma to group the users by the date they signed up and count how many signed up on each day, ordering the results by date.
Building the Backend (Express API)
This is our apps/backend/src/routes/signups.ts file:
import express from 'express';
import { PrismaClient } from '@prisma/client';
const router = express.Router();
const prisma = new PrismaClient();
router.get('/api/signups', async (req, res) => {
const data = await prisma.user.groupBy({
by: ['date'],
_count: { id: true },
orderBy: { date: 'asc' },
});
res.json(data.map(d => ({
date: d.date.toISOString().split('T')[0],
count: d._count.id,
})));
});
export default router;
This code sets up a simple web address (/api/signups) that, when you visit it, will use Prisma to get the signup data and send it back in a format the frontend can understand (date and count).
Making the Frontend (React + Chart.js)
This is our apps/frontend/src/App.tsx file:
import { useEffect, useState } from 'react';
import { Line } from 'react-chartjs-2';
function App() {
const [chartData, setChartData] = useState([]);
useEffect(() => {
fetch('/api/signups')
.then(res => res.json())
.then(setChartData);
}, []);
return (
<Line
data={{
labels: chartData.map(d => d.date),
datasets: [{ label: 'Signups', data: chartData.map(d => d.count) }],
}}
/>
);
}
export default App;
This React code fetches the signup data from our backend API when the app starts and then uses Chart.js to display it as a line chart.
Sharing Code (Types)
This is our packages/shared/types.ts file:
export interface SignupData {
date: string;
count: number;
}
We define a simple structure for our signup data. Now, both the frontend and backend can use this to make sure they're talking about the same thing:
import { SignupData } from '@shared/types';
Running Everything Together (Docker Compose)
This is our docker-compose.yml file:
version: '3.8'
services:
db:
image: postgres
environment:
POSTGRES_DB: appdb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- ./db:/docker-entrypoint-initdb.d
backend:
build: ./apps/backend
depends_on: [db]
environment:
DATABASE_URL: postgres://user:pass@db:5432/appdb
frontend:
build: ./apps/frontend
ports:
- "3000:3000"
depends_on: [backend]
This file tells your computer how to run the PostgreSQL database, the backend, and the frontend all at the same time. depends_on makes sure things start in the right order.
Super Fast Data Crunching (ClickHouse)
If you have tons of data and want to analyze it really quickly, you can use ClickHouse alongside PostgreSQL. You can use tools to automatically copy data from PostgreSQL to ClickHouse.
Why ClickHouse is Cool
- Blazing fast: It's designed to quickly count and group huge amounts of data.
- Great for history: Perfect for looking at trends over long periods.
- Plays well with others: You can use it with PostgreSQL as a separate place to do your analysis.
Here's an example of how you might set up a table in ClickHouse:
CREATE TABLE signups_daily
(
date Date,
count UInt32
)
ENGINE = MergeTree()
ORDER BY date;
Making Development Easier
- Turborepo or Nx: Tools to speed up building and testing different parts of your monorepo.
- ESLint and Prettier: Keep your code looking consistent with shared rules.
- Husky + Lint-Staged: Automatically check your code for style issues before you commit it.
- tsconfig.base.json: Share TypeScript settings across your projects.
Why This Is Awesome in Real Life
- One download: You only need to download the code once to get everything.
- One place for everything: Easier to manage updates and who owns different parts of the code.
- Fewer mistakes: Sharing code types helps catch errors early.
- Easy to get started: New team members can get up and running quickly.
Wrapping Up
Using a monorepo with React, Node.js, and PostgreSQL (with Prisma) is a smart way to build full-stack apps. It keeps things organized, makes development smoother, and sets you up for growth. Adding ClickHouse later on gives you powerful tools for understanding your data.
Whether you're building a small project or something that will grow big, this approach can make your life a lot easier.
What's Next?
- Add ways for users to log in securely (using tools like Clerk, Auth0, or Passport.js).
- Automatically copy data to ClickHouse for faster analysis.
- Add features like showing data in pages, saving data temporarily (caching with Redis), or letting users filter the data.
- Put your app online using services like Fly.io, Railway, Render, or Vercel (for the frontend).
Opinions expressed by DZone contributors are their own.
Comments