A full-stack TypeScript application with a React frontend and Express backend using tRPC for type-safe API communication. Implements a user feedback system with CRUD operations.
# 1. Install dependencies
pnpm install
# 2. Start the database (Docker required)
pnpm db:up
# 3. Push database schema
pnpm db:push
# 4. Run development servers
pnpm devOpen http://localhost:5173 to view the app.
| Tool | Version | Why |
|---|---|---|
| Node.js | v25+ | Runtime |
| pnpm | v10+ | Package manager |
| Docker | Latest | PostgreSQL database |
node --version # v25 or higher
pnpm --version # v10 or higher
docker --version # Any recent versiongit clone [email protected]:leonardoazeredo/searchland-take-home.git
cd searchland-take-home
pnpm installCopy the example environment file:
cp .env.example .envDefault values work out of the box:
- Database:
postgresql://postgres:password@localhost:5433/searchland - Server Port:
3001 - Client: Auto-detects server URL
pnpm db:upThis starts PostgreSQL on port 5433 using Docker.
Verify it's running:
docker ps # Should show postgres containerpnpm db:pushThis creates the feedback table with schema:
id(serial, primary key)name(varchar 255)email(varchar 255)message(text)createdAt(timestamp)
pnpm devThis runs both:
- Client: http://localhost:5173
- Server: http://localhost:3001
| Command | Description |
|---|---|
pnpm dev |
Start both client & server in dev mode |
pnpm build |
Build both apps for production |
pnpm lint |
Check code style with Biome |
pnpm lint:fix |
Auto-fix linting issues |
pnpm typecheck |
Type-check all packages |
pnpm db:up |
Start PostgreSQL container |
pnpm db:down |
Stop PostgreSQL container |
pnpm db:push |
Push schema changes to database |
searchland-take-home/
├── apps/
│ ├── client/ # React 19 + Vite + TailwindCSS 4
│ │ ├── src/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ ├── trpc.ts
│ │ │ └── main.tsx
│ │ └── package.json
│ │
│ └── server/ # Express 5 + tRPC
│ ├── src/
│ │ ├── index.ts
│ │ └── trpc.ts
│ └── package.json
│
├── packages/
│ ├── api/ # Shared tRPC routers
│ │ ├── src/
│ │ │ ├── router/
│ │ │ └── trpc.ts
│ │ └── package.json
│ │
│ └── db/ # Database schema & client
│ ├── src/
│ │ ├── schema.ts
│ │ └── index.ts
│ └── package.json
│
├── docker-compose.yml # PostgreSQL configuration
├── .env # Environment variables
├── .env.example # Example environment file
├── biome.json # Linting/formatting config
├── package.json # Root package.json
└── tsconfig.base.json # Shared TypeScript config
- React 19 - UI framework
- Vite - Build tool & dev server
- TailwindCSS 4 - Styling
- tRPC - Type-safe API client
- TanStack React Query - Data fetching
- Zod - Form validation
- Lucide React - Icons
- Express 5 - Web server
- tRPC - Type-safe API
- Drizzle ORM - Database ORM
- PostgreSQL 17 - Database
- Zod - Input validation
- TypeScript - Type safety
- Biome - Linting & formatting
- Docker - Database container
| Procedure | Method | Description |
|---|---|---|
feedback.getAll |
Query | Get all feedback (paginated, 20 per page) |
feedback.getById |
Query | Get single feedback by ID |
feedback.create |
Mutation | Create new feedback |
feedback.update |
Mutation | Update existing feedback |
feedback.delete |
Mutation | Delete feedback by ID |
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check with DB status |
feedback {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
email String @db.VarChar(255)
message String
createdAt DateTime @default(now())
deletedAt DateTime? // Soft delete (nullable)
@@index([createdAt])
@@index([email])
@@index([deletedAt])
}pnpm lint # Check for issues
pnpm lint:fix # Auto-fix issuespnpm buildOutput:
- Client:
apps/client/dist/ - Server:
apps/server/dist/
To ensure this project reflects how I would architect a production-ready application for a scaling team, I made a few intentional additions to the required stack:
- Vite (vs. Create React App / Next.js):
- Why: Unmatched developer experience (DX) and HMR speed. Since the prompt asked for a React SPA (and didn't explicitly require SSR/SEO), Vite is the most pragmatic choice for rapid iteration during the live coding stage.
- Trade-off: Lacks built-in file-system routing and SSR compared to Next.js, but React Router v7 handles the client-side routing perfectly for this scope.
- Zod:
- Why: Essential for tRPC. It provides runtime validation that perfectly infers to TypeScript types, ensuring end-to-end type safety across the network boundary.
- Trade-off: Adds a small amount to the client bundle size, but the guarantee of data integrity on the backend is worth the cost.
- Biome (vs. ESLint + Prettier):
- Why: A single, incredibly fast Rust-based toolchain that handles both linting and formatting. It removes the configuration overhead of making ESLint and Prettier play nicely together.
- Trade-off: Newer ecosystem with fewer community plugins than ESLint, but covers 99% of standard TypeScript/React needs out of the box.
- pnpm Workspaces (Monorepo):
- Why: Separating the
client,server,api(tRPC routers), anddbinto distinct packages enforces strict architectural boundaries. It prevents the frontend from accidentally importing server-only code (like Nodefsor DB credentials).
- Why: Separating the
AGPL-3.0