Building a Full-Stack TypeScript Monorepo with React and Hono

Somebody who likes to code
Search for a command to run...

Somebody who likes to code
No comments yet. Be the first to comment.
Modern API development requires balancing multiple concerns: runtime validation, compile-time type safety, and comprehensive documentation. Traditionally, these have been separate concerns, leading to documentation drift and type mismatches between s...
Google processes tens of thousands of code changes every day across a codebase of hundreds of millions of lines. The secret isn't magic tooling — it's a documented, principled approach to code review

Amazon Simple Notification Service (SNS) looks straightforward on the surface — publish a message, deliver it to subscribers — but the gap between a topic that "works in dev" and one that is productio

Amazon Simple Queue Service (SQS) is deceptively simple to get started with, but surprisingly easy to misconfigure in ways that cause silent message loss, runaway costs, duplicate processing storms, o

HTTP API is the right default choice for the majority of .NET serverless workloads. At 70% less cost than REST API, lower latency, and native JWT authorization, it covers most production requirements

AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read

On this page
This article guides you through creating a full-stack TypeScript monorepo from scratch. By the end, you'll have a React frontend and Hono API backend sharing the same repository with unified tooling for linting, formatting, and commit conventions.
Before starting, ensure you have installed:
Node.js 20 or higher
npm 10 or higher
Git
A monorepo (monolithic repository) is a software development strategy where multiple projects are stored in a single repository. Instead of having separate repositories for your frontend, backend, and shared libraries, everything lives together under one roof.
To understand monorepos, let's compare them with the traditional polyrepo approach:
Polyrepo (Multiple Repositories):
github.com/your-org/frontend → React application
github.com/your-org/backend → Hono API
github.com/your-org/shared-ui → Component library
github.com/your-org/utils → Shared utilities
Monorepo (Single Repository):
github.com/your-org/platform
├── apps/
│ ├── frontend/ → React application
│ └── backend/ → Hono API
└── packages/
├── shared-ui/ → Component library
└── utils/ → Shared utilities
In a JavaScript/TypeScript monorepo, package managers like npm, Yarn, or pnpm provide workspaces functionality. Workspaces allow you to:
Link packages locally: Instead of publishing @your-org/utils to npm and installing it in your frontend, the package manager creates symlinks between workspace packages. Changes are immediately available without publishing.
Hoist shared dependencies: Common dependencies (like TypeScript or React) are installed once at the root level and shared across all packages, reducing disk space and ensuring version consistency.
Run scripts across packages: Execute commands like npm run build across all packages or target specific workspaces with flags like -w @your-org/frontend.
While npm workspaces (which we'll use in this guide) provide basic monorepo functionality, specialized tools offer additional features:
| Tool | Description |
| npm/yarn/pnpm workspaces | Built-in workspace support in package managers |
| Turborepo | A high-performance build system focused on speed |
For this guide, we'll use npm workspaces as it requires no additional dependencies and covers the essential functionality needed for most projects.
Before we begin, let's understand the benefits of a monorepo architecture:
Shared tooling: Configure ESLint, Prettier, and TypeScript once for all packages
Atomic commits: Changes spanning the frontend and backend can be committed together
Simplified dependency management: Shared dependencies are hoisted to the root
Cross-package imports: Frontend can import types directly from backend
Unified CI/CD: A single pipeline handles all packages
Start by creating your project directory and initializing git:
mkdir node-monorepo
cd node-monorepo
git init
Create the folder structure for our monorepo:
mkdir -p apps/backend/src
mkdir -p apps/frontend/src
mkdir packages
We use the apps/packages convention, which is widely adopted in the JavaScript ecosystem:
apps/ contains deployable applications (our backend and frontend)
packages/ is reserved for shared libraries (we'll leave it empty for now)
Initialize the root package using npm:
npm init -y
Now open package.json and replace its contents with:
{
"name": "node-monorepo",
"version": "1.0.0",
"private": true,
"type": "module",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "concurrently \"npm:dev:backend\" \"npm:dev:frontend\"",
"dev:backend": "npm run dev -w @node-monorepo/backend",
"dev:frontend": "npm run dev -w @node-monorepo/frontend",
"build:backend": "npm run build -w @node-monorepo/backend",
"build:frontend": "npm run build -w @node-monorepo/frontend",
"build": "npm run build:backend && npm run build:frontend",
"start:backend": "npm run start -w @node-monorepo/backend",
"preview:frontend": "npm run preview -w @node-monorepo/frontend",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:format": "npm run lint:fix && npm run format",
"prepare": "husky || true",
"commit": "commit"
},
"devDependencies": {
"@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.1",
"@commitlint/prompt-cli": "^20.3.1",
"@eslint/js": "^9.18.0",
"concurrently": "^9.1.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^17.0.0",
"husky": "^9.1.7",
"prettier": "^3.4.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0"
}
}
Let's understand the key properties:
| Property | Purpose |
private: true | Prevents accidental publishing to npm |
type: "module" | Enables ES modules throughout the project |
workspaces | Defines npm workspaces—npm will link packages and hoist shared dependencies |
About the scripts:
dev: Runs both servers in parallel using concurrently. We can't use npm run dev --workspaces because that runs scripts sequentially, not in parallel.
-w @node-monorepo/backend: The -w flag targets a specific workspace by name.
prepare: Runs automatically after npm install to set up Husky. The || true prevents failures in CI environments where git might not be initialized.
About devDependencies:
All shared tooling (ESLint, Prettier, TypeScript, Husky, Commitlint) lives at the root. This ensures:
Consistent versions across all packages
Single source of truth for configuration
Reduced duplication in node_modules
In a monorepo, TypeScript configurations often share many options. Instead of duplicating them in every workspace, we create a base configuration that all others extend from.
Create tsconfig.base.json in the project root:
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"allowUnreachableCode": false,
"noErrorTruncation": true,
"noPropertyAccessFromIndexSignature": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}
Key options explained:
| Option | Purpose |
strict | Enables all strict type-checking options |
verbatimModuleSyntax | Enforces explicit type imports for better tree-shaking |
noUnusedLocals / noUnusedParameters | Catches unused variables and parameters |
noFallthroughCasesInSwitch | Prevents accidental fallthrough in switch statements |
noPropertyAccessFromIndexSignature | Forces bracket notation for index signatures, making dynamic access explicit |
forceConsistentCasingInFileNames | Prevents issues on case-sensitive file systems |
Now, create tsconfig.json that extends the base configuration. This config is specifically for the root-level config files (ESLint, Prettier, Commitlint):
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2023"],
"types": ["node"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true
},
"include": ["eslint.config.ts", "commitlint.config.ts", "prettier.config.ts"]
}
By using "extends": "./tsconfig.base.json", we inherit all the strict options from the base config and only specify what's unique to this context:
lib: ["ES2023"]: Standard library types (no DOM since these run in Node.js)
types: ["node"]: Node.js type definitions
noEmit: true: We're only type-checking, not compiling
strict: true: Enables all strict type-checking options
Navigate to the backend directory and initialize the package:
cd apps/backend
npm init -y
Open apps/backend/package.json and replace its contents with:
{
"name": "@node-monorepo/backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@hono/node-server": "^1.13.8",
"hono": "^4.6.18"
},
"devDependencies": {
"@types/node": "^25.0.9",
"tsx": "^4.19.4"
}
}
Key decisions:
Scoped name @node-monorepo/backend: Follows npm's scoped package convention. This prevents naming conflicts and makes workspace references clearer.
tsx for development: A TypeScript execution engine that provides fast compilation via esbuild and includes watch mode for automatic reloading.
Hono + @hono/node-server: Hono is a lightweight, fast web framework. The @hono/node-server adapter allows it to run on Node.js.
Create apps/backend/tsconfig.json that extends the base configuration:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"sourceMap": true,
"types": ["node"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"removeComments": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Notice how much shorter this is compared to a standalone config. By extending ../../tsconfig.base.json, we inherit all the strict options and only specify what's unique to the backend.
Important options explained:
| Option | Value | Purpose |
module | "NodeNext" | Proper Node.js ES module support |
moduleResolution | "NodeNext" | Matches the module system for correct resolution |
jsx | "react-jsx" | Enables JSX support (Hono has its own JSX runtime) |
jsxImportSource | "hono/jsx" | Uses Hono's JSX runtime instead of React |
sourceMap | true | Enables debugging in VS Code |
paths | {"#/*": ["./src/*"]} | Path alias for clean imports |
Why #/ for the path alias? We use #/ for backend and @/ for frontend to create a clear visual distinction. In the backend code, you can write:
import { someUtil } from '#/utils/helper';
// Instead of: import { someUtil } from '../../../utils/helper';
Create the main entry point at apps/backend/src/index.ts:
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serve } from '@hono/node-server';
const app = new Hono();
app.use(
'/api/*',
cors({
origin: 'http://localhost:5173',
})
);
app.get('/api/hello', c => {
return c.json({ message: 'Hello World from Hono!' });
});
const port = 3000;
console.log(`Server is running on http://localhost:${port}`);
serve({
fetch: app.fetch,
port,
});
Let's break down this code:
CORS middleware: The frontend runs on port 5173 (Vite's default), so we explicitly allow that origin. Without this, the browser would block requests from the frontend to the backend due to the same-origin policy.
/api/* prefix: Prefixing all API routes with /api is a common convention that:
Makes it easy to distinguish API calls from static assets
Simplifies reverse proxy configuration in production
Allows CORS to be applied only to API routes
/api/hello endpoint: A simple GET endpoint that returns a JSON message. The c parameter is Hono's context object, which provides request/response utilities.
serve() function: The @hono/node-server adapter that starts the HTTP server.
Navigate to the frontend directory and initialize the package:
cd apps/frontend
npm init -y
Open apps/frontend/package.json and replace its contents with:
{
"name": "@node-monorepo/frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^5.1.2",
"vite": "^7.3.1"
}
}
Why separate dependencies from the root? Runtime dependencies (react, react-dom) are placed in each workspace because:
They are specific to that application
They will be bundled into the final build
Different apps might need different versions
The frontend uses a multi-file TypeScript configuration pattern recommended by Vite. This allows separate settings for browser code and Node.js code (like vite.config.ts).
Create apps/frontend/tsconfig.json:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
This file doesn't compile anything—it orchestrates the other configs using project references. This enables:
Faster incremental builds
Separate configurations for browser and Node.js code
Better IDE support
Create apps/frontend/tsconfig.app.json for browser code:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"],
"#/*": ["../backend/src/*"]
}
},
"include": ["src"]
}
Key points:
extends: Inherits strict options from the base config
lib: ["ES2022", "DOM", "DOM.Iterable"]: Includes browser APIs (DOM)
moduleResolution: "bundler": Optimized for bundlers like Vite
paths: Defines two aliases:
@/* for internal frontend imports
#/* for importing backend types (cross-workspace)
Cross-workspace type imports: The #/* path pointing to ../backend/src/* allows the frontend to import types directly from the backend:
// In frontend, you could import backend types like this:
import type { SomeApiResponse } from '#/types';
Create apps/frontend/tsconfig.node.json for Vite config:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2023"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true
},
"include": ["vite.config.ts"]
}
This config is much smaller because it extends the base. It exists separately because vite.config.ts runs in Node.js, not the browser—notice there's no DOM in the lib array.
Create apps/frontend/vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'#': path.resolve(__dirname, '../backend/src'),
},
},
});
Important: Path aliases must be defined in both tsconfig.app.json (for TypeScript type checking) and vite.config.ts (for the bundler's module resolution). TypeScript handles type checking, while Vite handles the actual import resolution during development and build.
Create apps/frontend/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Node Monorepo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Vite uses index.html as the entry point. The <script type="module"> tag points to our main TypeScript file.
Create the main entry file at apps/frontend/src/main.tsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
StrictMode helps identify potential problems by activating additional checks during development.
Create the App component at apps/frontend/src/App.tsx:
import { useEffect, useState } from 'react';
function App() {
const [message, setMessage] = useState<string>('Loading...');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('http://localhost:3000/api/hello')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch');
}
return response.json();
})
.then(data => {
setMessage(data.message);
})
.catch(err => {
setError(err.message);
});
}, []);
return (
<div className="container">
<h1>Node Monorepo</h1>
{error ? (
<p className="error">Error: {error}</p>
) : (
<p className="message">{message}</p>
)}
</div>
);
}
export default App;
This component:
Uses useState to manage the message and error state
Uses useEffect to fetch data from the backend when the component mounts
Displays either the message or an error
Create the styles at apps/frontend/src/index.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
h1 {
color: #333;
margin-bottom: 1rem;
}
.message {
font-size: 1.25rem;
color: #667eea;
}
.error {
color: #e53e3e;
}
ESLint 9 introduced the flat config format, replacing the legacy .eslintrc files. Create eslint.config.ts at the project root:
import jseslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';
import prettierConfig from 'eslint-config-prettier';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import globals from 'globals';
export default defineConfig(
globalIgnores(['**/dist/**/*', '**/node_modules/**/*', '**/*.tsbuildinfo']),
jseslint.configs.recommended,
tseslint.configs.recommended,
prettierConfig,
{
files: ['eslint.config.ts', 'commitlint.config.ts', 'prettier.config.ts'],
languageOptions: {
globals: globals.node,
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './tsconfig.json',
},
},
},
{
files: ['apps/backend/**/*.{ts,tsx}'],
languageOptions: {
globals: globals.node,
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './apps/backend/tsconfig.json',
},
},
},
{
files: ['apps/frontend/src/**/*.{ts,tsx}'],
extends: [reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
languageOptions: {
globals: globals.browser,
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './apps/frontend/tsconfig.app.json',
},
},
},
{
files: ['apps/frontend/vite.config.ts'],
languageOptions: {
globals: globals.node,
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './apps/frontend/tsconfig.node.json',
},
},
}
);
Let's understand each part:
globalIgnores(['**/dist/**/*', '**/node_modules/**/*', '**/*.tsbuildinfo']),
This replaces the old .eslintignore file. We ignore:
dist/: Build output directories
node_modules/: Dependencies
*.tsbuildinfo: TypeScript incremental compilation cache
jseslint.configs.recommended,
tseslint.configs.recommended,
prettierConfig,
These apply to all files:
jseslint.configs.recommended: ESLint's recommended JavaScript rules
tseslint.configs.recommended: TypeScript-specific rules
prettierConfig: Must come after other configs to disable rules that conflict with Prettier
Each block targets specific files with appropriate settings:
Root config files:
{
files: ['eslint.config.ts', 'commitlint.config.ts', 'prettier.config.ts'],
languageOptions: {
globals: globals.node, // Node.js globals (process, __dirname, etc.)
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './tsconfig.json', // Points to root tsconfig
},
},
},
Backend files:
{
files: ['apps/backend/**/*.{ts,tsx}'],
languageOptions: {
globals: globals.node,
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './apps/backend/tsconfig.json',
},
},
},
Frontend source files:
{
files: ['apps/frontend/src/**/*.{ts,tsx}'],
extends: [reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
languageOptions: {
globals: globals.browser, // Browser globals (window, document, etc.)
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './apps/frontend/tsconfig.app.json',
},
},
},
Note the React-specific plugins:
reactHooks.configs.flat.recommended: Enforces Rules of Hooks
reactRefresh.configs.vite: Ensures components are compatible with hot module replacement
Vite config file:
{
files: ['apps/frontend/vite.config.ts'],
languageOptions: {
globals: globals.node, // Vite config runs in Node.js
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: './apps/frontend/tsconfig.node.json',
},
},
},
Why specify a project for each file pattern? TypeScript-ESLint can provide type-aware linting when it knows which tsconfig.json applies to each file. This enables catching more errors like unused variables that TypeScript alone might not flag.
Important: You must use eslint-plugin-react-hooks version 7 or higher. Earlier versions don't export a flat config (configs.flat.recommended is only available in v7+).
Create prettier.config.ts at the project root:
import type { Config } from 'prettier';
const config: Config = {
trailingComma: 'es5',
singleQuote: true,
arrowParens: 'avoid',
endOfLine: 'crlf',
};
export default config;
Options explained:
| Option | Value | Purpose |
trailingComma | 'es5' | Adds trailing commas in objects and arrays. Creates cleaner git diffs when adding items. |
singleQuote | true | Uses single quotes for strings (common JavaScript convention) |
arrowParens | 'avoid' | Omits parentheses for single-parameter arrow functions: x => x instead of (x) => x |
endOfLine | 'crlf' | Windows line endings. Use 'lf' for Unix/macOS teams. |
Create .prettierignore at the project root to exclude generated files:
**/dist/
**/*.tsbuildinfo
**/package-lock.json
Why ignore these?
dist/: Generated build output—formatting would be overwritten on next build
*.tsbuildinfo: Binary cache files
package-lock.json: Auto-generated by npm, formatting changes create noise in git history
Commitlint ensures all commit messages follow a consistent format. Create commitlint.config.ts at the project root:
import type { UserConfig } from '@commitlint/types';
const config: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': [2, 'always', ['backend', 'frontend', 'repo']],
'subject-case': [2, 'always', ['sentence-case', 'lower-case']],
},
};
export default config;
Understanding rule format: [level, applicable, value]
Level: 0 = disabled, 1 = warning, 2 = error
Applicable: 'always' (must match) or 'never' (must not match)
Value: The rule configuration
Our rules:
scope-enum: Restricts commit scopes to predefined values:
feat(backend): Add user authentication ✓
fix(frontend): Resolve button styling ✓
chore(repo): Update dependencies ✓
feat(api): Add endpoint ✗ ('api' not in allowed scopes)
subject-case: Allows either sentence case or lowercase:
feat: Add new feature ✓
feat: add new feature ✓
feat: ADD NEW FEATURE ✗
The conventional commit format is:
<type>(<scope>): <subject>
[optional body]
[optional footer]
Common types:
| Type | When to use |
feat | New feature |
fix | Bug fix |
docs | Documentation changes |
style | Code style changes (formatting, semicolons) |
refactor | Code changes that neither fix bugs nor add features |
test | Adding or modifying tests |
chore | Maintenance tasks (dependencies, build config) |
Create the .husky directory and hook files:
mkdir -p .husky
Create .husky/pre-commit with the following content:
npm run lint
npm run format:check
This runs ESLint and checks Prettier formatting before each commit. If either fails, the commit is aborted, ensuring only properly formatted and linted code enters the repository.
Create .husky/commit-msg with the following content:
npx commitlint --edit $1
This validates the commit message against your commitlint rules. The $1 argument is the path to the temporary file containing the commit message.
Make the hooks executable (Unix/macOS):
chmod +x .husky/pre-commit
chmod +x .husky/commit-msg
From the project root, run:
npm install
npm workspaces will:
Install root devDependencies
Install each workspace's dependencies
Hoist shared dependencies to the root node_modules
Create symlinks for workspace packages
Run the prepare script, initializing Husky
Run ESLint to check for errors:
npm run lint
Check Prettier formatting:
npm run format:check
If there are formatting issues, fix them:
npm run format
npm run dev
This runs both servers concurrently:
Backend at http://localhost:3000
Frontend at http://localhost:5173
Open your browser to http://localhost:5173. You should see the "Node Monorepo" heading with the message "Hello World from Hono!" fetched from the backend.
npm run build
This builds both the backend (TypeScript compilation) and frontend (Vite production build).
Try making a commit to verify the hooks work:
git add .
git commit -m "feat(repo): Initial monorepo setup"
The pre-commit hook will run lint and format checks. The commit-msg hook will validate your commit message format.
node-monorepo/
├── apps/
│ ├── backend/
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json # Extends tsconfig.base.json
│ └── frontend/
│ ├── src/
│ │ ├── App.tsx
│ │ ├── main.tsx
│ │ └── index.css
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json # Project references
│ ├── tsconfig.app.json # Extends tsconfig.base.json
│ ├── tsconfig.node.json # Extends tsconfig.base.json
│ └── vite.config.ts
├── packages/
├── .husky/
│ ├── pre-commit
│ └── commit-msg
├── package.json
├── tsconfig.base.json # Shared TypeScript options
├── tsconfig.json # Extends tsconfig.base.json
├── eslint.config.ts
├── prettier.config.ts
├── commitlint.config.ts
└── .prettierignore
This template provides a solid foundation for building full-stack TypeScript applications with modern tooling and best practices. You can find all the code here. Thanks, and happy coding