Monorepo with React frontend + Express backend, deployed on Railway via Docker. Single app architecture: Backend serves both API endpoints and frontend static files.
live-code/
├── apps/
│ ├── backend/ # Express API (serves frontend static files)
│ │ ├── src/
│ │ │ ├── index.ts # Main server entry point
│ │ │ └── *.test.ts # Test files (colocated)
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── eslint.config.js
│ └── frontend/ # React SPA (Vite)
│ ├── src/
│ │ ├── main.tsx # React entry point
│ │ ├── App.tsx # Main component
│ │ └── App.css # Styles
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ └── eslint.config.js
├── .github/workflows/ # CI/CD
├── Dockerfile # Multi-stage build (frontend + backend)
└── railway.json # Railway deployment config
# Install all dependencies (backend + frontend)
npm install
# Development (backend + frontend in parallel)
npm run dev
# Build all
npm run build
# Lint all
npm run lint
# Typecheck all
npm run typecheck
# Run tests (backend only)
npm run testcd apps/backend
# Install dependencies
npm install
# Development (hot reload)
npm run dev
# Build for production
npm run build
# Type checking
npm run typecheck
# Linting
npm run lint
# Run all tests
npm run test
# Run a single test file
npx vitest run src/index.test.ts
# Run tests matching a pattern
npx vitest run -t "should pass"
# Watch mode
npx vitestcd apps/frontend
# Install dependencies
npm install
# Development server (with API proxy to backend)
npm run dev
# Build for production
npm run build
# Type checking
npm run typecheck
# Linting
npm run lint
# Preview production build
npm run preview- Language: All code, comments, and commit messages in English
- Module system: ES Modules (
"type": "module"in package.json) - Target: ES2022 for backend, ES2020 for frontend
- Strict mode: TypeScript strict mode enabled
Order imports as follows (separated by blank lines):
- Node.js built-in modules
- External dependencies
- Internal modules
- Types (if separate)
// Good
import path from "path";
import { fileURLToPath } from "url";
import express, { Request, Response } from "express";
import { myUtil } from "./utils.js";- Strict typing: Avoid
any, use proper types - Interfaces over types: Prefer
interfacefor object shapes - Explicit return types: Optional but recommended for public functions
- Unused variables: Prefix with underscore
_reqto ignore
// Good - unused parameter prefixed with _
app.get("/api/health", (_req: Request, res: Response) => {
res.json({ status: "ok" });
});| Element | Convention | Example |
|---|---|---|
| Files | kebab-case | user-service.ts |
| Variables/Functions | camelCase | getUserById |
| Classes/Interfaces | PascalCase | ApiResponse |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES |
| Test files | *.test.ts |
index.test.ts |
- Functional components: No class components
- Hooks: Follow rules of hooks (enforced by ESLint)
- State: Use
useStatefor local state - Effects: Use
useEffectwith proper dependencies
// Good
function App() {
const [data, setData] = useState<ApiResponse | null>(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data?.message}</div>;
}- Backend: Return appropriate HTTP status codes
- Frontend: Catch errors and display user-friendly messages
- Never swallow errors: Always log or handle appropriately
// Backend - explicit error responses
app.get("/api/resource", async (req, res) => {
try {
const data = await fetchData();
res.json(data);
} catch (error) {
console.error("Failed to fetch:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Frontend - catch and display
fetch("/api/data")
.then(res => res.json())
.catch(err => setError(err.message));- Backend: Vitest
- Test location: Colocate tests with source files (
src/index.test.ts)
import { describe, it, expect } from "vitest";
describe("FeatureName", () => {
it("should do something specific", () => {
expect(result).toBe(expected);
});
});# Single file
npx vitest run src/index.test.ts
# By test name pattern
npx vitest run -t "should return ok"
# Watch mode for TDD
npx vitestOn push/PR to main:
- Backend:
npm install→typecheck→lint→test - Frontend:
npm install→typecheck→lint
- Platform: Railway (auto-deploy on push to
main) - Build: Docker multi-stage (see
Dockerfile) - No manual deploy workflow: Railway handles deployment automatically
-
Single app architecture: Backend serves frontend static files. No separate deployments.
-
API routes: All API endpoints under
/api/*. Frontend uses relative URLs (/api/health). -
Port handling: Use
process.env.PORT(Railway injects this). Default to3001for local dev. -
Frontend path: In production, frontend is served from
../../frontend/distrelative to backenddist/. -
No package-lock.json: Using
npm install(notnpm ci). Don't commit lock files. -
Vitest for testing: Not Jest. Use
vitestimports. -
ESLint flat config: Using new
eslint.config.jsformat, not.eslintrc.