A reusable full-stack TypeScript monorepo starter for building web applications with a React frontend, Hono API, PostgreSQL database, and shared TypeScript packages.
The goal of this starter is to provide the common infrastructure needed for a modern TypeScript application without adding application-specific architecture or unnecessary dependencies.
- React
- Vite
- TypeScript
- Tailwind CSS
- shadcn/ui
- Base UI
- Lucide icons
- Hono
- Node.js
- TypeScript
- Zod
- Kysely
- PostgreSQL
- pnpm workspaces
- ESLint
- Prettier
- Vitest
.
├── apps/
│ ├── api/
│ │ ├── src/
│ │ │ ├── config/
│ │ │ ├── db/
│ │ │ │ └── migrations/
│ │ │ ├── app.ts
│ │ │ ├── app.test.ts
│ │ │ └── server.ts
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.build.json
│ │
│ └── web/
│ ├── src/
│ │ ├── components/
│ │ │ └── ui/
│ │ ├── lib/
│ │ ├── App.tsx
│ │ └── index.css
│ ├── components.json
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
│
├── packages/
│ ├── shared/
│ │ ├── src/
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.build.json
│ │
│ └── typescript-config/
│ ├── base.json
│ ├── node.json
│ ├── react.json
│ └── package.json
│
├── .env.example
├── .gitignore
├── .prettierignore
├── .prettierrc
├── eslint.config.mjs
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── README.md
Install the following before setting up the project:
- Node.js
- pnpm
- PostgreSQL
git clone <repository-url>
cd <project-directory>pnpm installpnpm installs dependencies for all applications and packages in the workspace using the root pnpm-lock.yaml.
Copy the example environment file:
cp .env.example .envThe default example configuration is:
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app_developmentUpdate DATABASE_URL to match your local PostgreSQL configuration.
The real .env file is ignored by Git and should not be committed.
Create a database matching the database name configured in DATABASE_URL.
For example:
CREATE DATABASE app_development;For a real application, consider creating a dedicated PostgreSQL role and database instead of using the default postgres user.
For example:
CREATE ROLE app_dev_user
WITH LOGIN PASSWORD 'dev_password';
CREATE DATABASE app_development
OWNER app_dev_user;The corresponding environment variable would be:
DATABASE_URL=postgresql://app_dev_user:dev_password@localhost:5432/app_developmentThese credentials are only examples. Choose appropriate credentials for your own development environment.
pnpm --filter @starter/api db:migrateThe starter intentionally contains no application-specific migrations.
Add migrations to:
apps/api/src/db/migrations/
pnpm devThis starts the API and frontend development servers in parallel.
By default:
- API:
http://localhost:3000 - API health check:
http://localhost:3000/health - Web: Vite displays the local development URL in the terminal
Open the Vite URL in your browser to access the frontend.
Run the following commands from the repository root.
pnpm devStarts the API and web development servers in parallel.
pnpm typecheckRuns TypeScript type checking across the workspaces.
pnpm testRuns tests in every workspace that defines a test script.
pnpm lintRuns ESLint across the repository.
Format the repository with Prettier:
pnpm formatCheck formatting without modifying files:
pnpm format:checkThe check command is useful for CI because it reports formatting problems without changing source files.
pnpm buildThe build process first builds the shared package:
packages/shared
It then builds the API and web applications.
The shared package must be built first because the applications consume its compiled output.
After building:
pnpm --filter @starter/api startThe production API runs the compiled JavaScript from:
apps/api/dist/
This repository uses pnpm workspaces.
The workspace configuration is defined in:
pnpm-workspace.yaml
Applications live under:
apps/*
Reusable packages live under:
packages/*
Workspace packages reference each other using pnpm's workspace:* protocol.
For example:
{
"dependencies": {
"@starter/shared": "workspace:*"
}
}This tells pnpm to use the local workspace package rather than downloading a package with the same name from a registry.
The backend application lives in:
apps/api/
It uses Hono running on Node.js.
The Hono application is defined in:
apps/api/src/app.ts
The application is intentionally kept separate from the HTTP server.
This makes the application easier to test because tests can make requests directly against the Hono app without starting a real TCP server.
The Node.js server entry point is:
apps/api/src/server.ts
This file is responsible for starting the HTTP server and reading the configured port.
The starter includes a simple health endpoint:
GET /health
A successful response looks like:
{
"status": "ok"
}The health endpoint provides a simple way to verify that the API is running.
Environment configuration is validated with Zod.
The environment schema lives in:
apps/api/src/config/env.ts
Expected variables include:
NODE_ENV
PORT
DATABASE_URL
The application fails early when required configuration is invalid or missing rather than failing later during application execution.
Database access uses PostgreSQL with Kysely.
The database client lives in:
apps/api/src/db/client.ts
Database types live in:
apps/api/src/db/database.types.ts
The Database interface is intentionally empty in the starter.
Add application tables as the database schema is designed.
For example:
export interface Database {
users: UserTable
}The exact table definitions should belong to the application built from this starter rather than the starter itself.
Kysely migrations live in:
apps/api/src/db/migrations/
Run all pending migrations with:
pnpm --filter @starter/api db:migrateThe migration runner is located at:
apps/api/src/db/migrate.ts
The starter contains no domain-specific migrations.
The frontend lives in:
apps/web/
It uses:
- React
- Vite
- TypeScript
- Tailwind CSS
- shadcn/ui
- Base UI
The main application component is:
apps/web/src/App.tsx
shadcn/ui configuration lives in:
apps/web/components.json
UI components are installed into:
apps/web/src/components/ui/
To add another component, run the shadcn CLI from the repository root.
For example:
pnpm dlx shadcn@latest add card -c apps/webThe starter keeps UI components inside the web application.
A separate shared UI package should only be introduced if multiple frontend applications eventually need to share the same components.
The web application supports the @/ path alias.
Instead of:
import { Button } from '../../../components/ui/button'use:
import { Button } from '@/components/ui/button'The alias points to:
apps/web/src/
It is configured for both TypeScript and Vite so the compiler, development server, production build, and tooling resolve imports consistently.
The shared package lives in:
packages/shared/
It is intended for code that genuinely needs to be consumed by both the frontend and backend.
Good candidates include:
- Zod schemas
- API request schemas
- API response schemas
- Shared TypeScript types
- Shared constants
- Shared API contracts
For example, an application might eventually define:
import { z } from 'zod'
export const createUserSchema = z.object({
name: z.string().min(1),
email: z.email(),
})
export type CreateUserInput = z.infer<typeof createUserSchema>Both the frontend and API could then consume the same contract.
Avoid putting server-only or browser-only application logic in the shared package.
Reusable TypeScript configuration lives in:
packages/typescript-config/
The package provides separate configurations for different environments.
base.json
node.json
react.json
base.json contains common strict TypeScript settings.
node.json extends the base configuration for the Node.js API.
react.json extends the base configuration for the React frontend.
This keeps important TypeScript settings consistent without duplicating the entire configuration across applications.
The API uses Vitest.
The starter includes a health endpoint test in:
apps/api/src/app.test.ts
The test calls the Hono application directly rather than starting the HTTP server.
Run all tests with:
pnpm testAs application features are added, keep tests close to the code they verify when practical.
ESLint configuration is centralized at:
eslint.config.mjs
The root configuration handles:
- TypeScript
- Node.js
- React
- React Hooks
- React Fast Refresh
Run ESLint with:
pnpm lintPrettier configuration is defined in:
.prettierrc
Format the repository with:
pnpm formatVerify formatting with:
pnpm format:checkESLint is responsible for code-quality rules.
Prettier is responsible for formatting.
TypeScript is responsible for type checking.
Keeping these responsibilities separate makes the development tooling easier to understand and maintain.
Run:
pnpm buildThe build order is:
shared package
↓
API + Web
The API and web applications can build in parallel after the shared package has been compiled.
Build output is generated under each application's or package's dist directory.
These directories are ignored by Git.
Before committing significant changes, run:
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm buildTogether these check:
- formatting
- lint rules
- TypeScript correctness
- automated tests
- production compilation
For local development:
pnpm devThen verify:
http://localhost:3000/health
returns:
{
"status": "ok"
}After creating a new repository from this starter:
Change the root package name in:
package.json
The starter uses:
@starter/api
@starter/web
@starter/shared
@starter/typescript-config
You can leave these names in place or replace @starter with a project-specific scope.
If you rename the scope, update all workspace references and root scripts.
pnpm installcp .env.example .envUpdate the database connection and any application-specific configuration.
Create a PostgreSQL development database for the application.
pnpm --filter @starter/api db:migratepnpm devAdd application-specific:
- database migrations
- schemas
- API routes
- services
- repositories
- React features
- tests
as the project requires them.
This starter intentionally avoids installing every library that might eventually be useful.
Add dependencies when the application has a real requirement for them.
Examples include:
- TanStack Query for server-state management
- Zustand for client-side state management
- Better Auth or another authentication solution
- OpenAPI tooling for documented APIs
- Pino for structured logging
- Playwright for end-to-end testing
- Docker for containerized environments
- queues and background jobs
- email providers
- file storage
- observability tooling
This keeps new projects small and avoids committing to architectural decisions before the application needs them.
This starter provides infrastructure, not a complete application architecture.
It deliberately does not include domain-specific folders such as:
customers/
orders/
inventory/
tires/
appointments/
Those belong in applications built from the starter.
Similarly, architectural patterns such as repositories, services, vertical slices, or feature modules should be introduced according to the complexity of the application.
The starter should remain generic enough to support different kinds of projects.
The starter follows a few principles:
Keep the foundation reusable.
Infrastructure that nearly every project needs belongs here.
Keep domain logic out.
Business rules belong to the application created from the starter.
Add dependencies when they solve a real problem.
Avoid turning the starter into a kitchen-sink framework.
Keep development and production behavior close.
Production builds should be tested rather than assuming development tooling behaves identically.
Prefer explicit configuration over unnecessary abstraction.
The monorepo is intentionally small enough that its build order and workspace relationships remain easy to understand without additional orchestration tooling.
Add an appropriate license before distributing the starter publicly.