A modern full-stack TypeScript monorepo using:
- pnpm workspaces
- Express + TypeScript backend
- Prisma ORM with SQLite (easily switchable to Postgres/MySQL)
- Vite + React + Tailwind frontend
- Full CRUD Todo App
- CORS-ready, Production-ready static serving
- Shared development workflow (pnpm dev)
TS_MonoRepo/
β
βββ backend/ # Express + Prisma + SQLite API
β βββ src/
β β βββ controllers/ # Route logic (Todo CRUD)
β β βββ routes/ # Express routers
β β βββ index.ts # Server bootstrap
β β βββ middleware/ # Error handling, etc.
β βββ prisma/
β β βββ schema.prisma # DB schema (Prisma)
β βββ package.json
β βββ tsconfig.json
β
βββ frontend/ # Vite + React + Tailwind UI
β βββ src/
β β βββ App.tsx # Minimal dark-theme Todo UI
β β βββ ...
β βββ index.html
β βββ package.json
β βββ tailwind config
β
βββ pnpm-workspace.yaml # Workspaces definition
βββ tsconfig.base.json
βββ package.json # Root scripts (dev/build)
- TypeScript
- Express
- Prisma ORM
- SQLite (dev) β upgradeable to PostgreSQL/MySQL
- tsx (dev runtime)
- CORS + Cookie Parser
- React + TypeScript
- Vite
- Tailwind CSS
- Minimal dark-theme UI
- CRUD operations calling
/api/todos
Make sure you are using pnpm.
pnpm installThis installs deps for both frontend and backend via workspaces.
Start both frontend & backend with one command:
pnpm dev- Frontend β http://localhost:3000
- Backend β http://localhost:8000
- Vite proxies
/apiβ backend automatically
Located at:
backend/prisma/schema.prisma
To generate the Prisma client:
cd backend
pnpm exec prisma generateRun migrations (development):
pnpm exec prisma migrate dev --name initJust update the datasource block:
Postgres:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}MySQL:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}And set the correct DATABASE_URL in production.
No backend code changes required.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/todos | List all todos |
| POST | /api/todos | Create new todo |
| GET | /api/todos/:id | Get a todo |
| PUT | /api/todos/:id | Update a todo |
| DELETE | /api/todos/:id | Delete a todo |
| GET | /api/health | Health check |
Backend checks NODE_ENV and serves static assets in production.
A minimal dark-theme Todo app is included.
- Fetches todos from
/api/todos - Create / edit / toggle / delete
- Automatically updates UI
Start only frontend:
pnpm --filter frontend devBuild frontend:
pnpm --filter frontend buildpnpm --filter frontend buildpnpm --filter backend buildbackend/src/index.ts automatically:
- Serves
/frontend/dist - Sets SPA fallback for non-API GET requests
- Listens on
0.0.0.0in production
Create backend/.env:
NODE_ENV=development
PORT=8000
CORS_ORIGIN=http://localhost:3000
DATABASE_URL="file:./dev.db"
In production use:
NODE_ENV=production
CORS_ORIGIN=https://yourfrontend.com,https://cdn.yourfrontend.com
DATABASE_URL="postgresql://user:password@host:5432/dbname"
Because index.ts exports app, you can test easily:
import request from "supertest";
import app from "../src/index.js";
test("GET /api/health", async () => {
const res = await request(app).get("/api/health");
expect(res.status).toBe(200);
});You can deploy backend + frontend in many environments:
- Build both frontend and backend
- Serve React static files via Express
- Run Node server (pm2 recommended)
- Use Prisma Data Proxy or Pooler
- Export
appto adapt to handler
- Upload
frontend/distto a CDN - API served separately from a VPS / container
- Set
CORS_ORIGINaccordingly
This is a learning-friendly project but follows production-grade patterns:
- Proper CORS handling
- Explicit ESM imports (
.js) for Node compatibility - Prisma migrations committed
- pnpm workspace structure
- Clean Express app organization (controllers / routes / middleware)
- Authentication (JWT or sessions)
- Zod validation (backend)
- React Query for better frontend state handling
- Docker setup
- PostgreSQL migration example
- Unit & integration tests
- CI/CD (GitHub Actions)
- A better styled frontend UI (Tailwind components)
- A proper Todos API test suite (Supertest + Vitest)
There this could be a few mistakes in readme