BrickMatch
A full-stack app that matches owned LEGO parts against the Rebrickable set catalog. It shows which sets you are closest to completing and what parts are still missing. The core is a matching engine: index-backed candidate search plus one aggregated SQL query, not a naive per-set scan or an AI brick scanner.
- Next.js
- NestJS
- TypeScript
- Drizzle ORM
- Neon Postgres
- Zod
- pnpm workspaces
- TanStack Query
- nuqs
- Tailwind CSS
- shadcn/ui
- Vitest
- Jest
Project Preview
Context
LEGO is a real personal interest for me. I kept finding old bricks or buying mixed part lots and not knowing what they could complete. Existing tools either scan bricks with AI, which is unreliable on color and part recognition, or they never answer the simple question of what I am closest to finishing. I wanted to build that matching piece properly over a large real dataset, instead of wrapping another CRUD app in AI.
Technical Approach
- NestJS API and Next.js frontend as separate deployables in a pnpm workspace monorepo, sharing one Zod schema package for types and validation
- PostgreSQL B-tree index on (part_num, color_id) narrows the full set catalog to realistic candidates before any percentage calculation runs
- Match percentage computed for all candidates in a single grouped SQL query with a HAVING clause, rather than one query per candidate
- Catalog data imported from Rebrickable's CSV export up front. The backend never calls a third-party API on the request path. Part and set images are hotlinked from Rebrickable's CDN in the browser.
- JWT delivered via an httpOnly cookie and validated by Nest Passport on the API. The frontend never reads the token and derives auth state from a /me query via TanStack Query.
- Server state in TanStack Query only. URL filter state (theme, min match %) owned by nuqs.
Challenges
- Rebrickable keeps minifig parts in a separate inventory graph (inventories → inventory_minifigs → inventory_parts), not on the owning set's inventory. I resolved that at import time by expanding minifig components into the parent set.
- BrickLink and Rebrickable use different internal IDs for parts and colors, with no clean downloadable mapping. I exported Rebrickable-format CSV and routed BrickLink conversion through Rebrickable's own tool instead of maintaining a mapping table.
- Tuning a single minimum-match-percentage threshold that behaves sensibly across very different set sizes, from a few dozen parts to several thousand
- Paginating a user's growing collection correctly while leaving small fixed reference data (colors, themes) unpaginated
What I Learned
- For large catalogs, the useful work belongs in the index and a single SQL aggregation. Looping over tens of thousands of sets in application code is a design smell.
- Expand nested catalog complexity such as minifigs at import time so runtime matching stays a flat (part_num, color_id) problem. Denormalize once, query simply.
- Shared Zod schemas remove a whole class of frontend/backend drift. A short custom Nest validation pipe was enough. nestjs-zod was optional complexity, not a requirement.
- Avoid building and maintaining cross-vendor ID mappings when an upstream converter already exists. Shipping a Rebrickable CSV export was the smaller surface.
- Pagination is a product decision. Growing user collections need it. Tiny static reference tables like colors and themes are better fetched whole and cached.
Key Features
- Index-backed matching engine over a 20,000+ set catalog
- Shared Zod schemas between frontend and backend via a pnpm workspace package
- Cookie-based JWT auth with password confirmation on sensitive actions
- Missing-parts CSV in Rebrickable format, then BrickLink XML via Rebrickable's converter, without a hand-built ID mapping
- Import-time minifig expansion into set inventories so matching stays a flat parts query
- Atomic build-set action that subtracts required parts from the collection only when the user can complete the set
Next Project
TextForge
Live text conversion tool with AI formatting, schema inference, and Stripe premium access.