Spinjitzu API
Ninjago has a large lore surface and almost no structured way to query it. Spinjitzu API is a public REST API I built for that gap: characters, seasons, elements, weapons, locations, and realms, with filtering, pagination, and OpenAPI docs. I treated it as a public service with consistent response envelopes, validation, JWT-protected admin writes disabled in production, rate limiting, tests, Docker for local work, and a serverless deploy.
- NestJS
- TypeScript
- PostgreSQL
- Neon
- Drizzle ORM
- Passport
- Zod
- class-validator
- Jest
- Docker
- Swagger / OpenAPI
Project Preview
Context
I have followed Ninjago for years. When I looked for an API to query characters, seasons, elements, and the rest of that universe, there was nothing usable, so I built one. The theme is personal, but the engineering still had to hold up: clear contracts, auth boundaries, rate limits, docs, and a deploy I would trust.
Technical Approach
- Feature-first module structure: Controller → Service → Drizzle → PostgreSQL, with a repository layer omitted until it proves necessary
- Neon serverless Postgres via the HTTP driver, chosen for compatibility with serverless deployment on Vercel
- Centralized response shaping with a global interceptor and exception filter, so every endpoint returns the same success and error envelope
- Composed decorators such as @AdminWrite() that bundle JWT auth, role checks, write-tier throttling, and production disable/hide rules for every write endpoint
Challenges
- Modeling many-to-many relationships between characters, elements, weapons, and seasons so the data stays accurate as lore changes owners, powers, and eras over time
- Diagnosing a NestJS API-versioning and global-prefix conflict that broke the unversioned root endpoint. Solved with VERSION_NEUTRAL routing
- Getting Swagger UI static assets to load on Vercel's serverless bundler by serving CSS and JS from a CDN instead of the package's dynamic paths
- Deciding what to disable versus merely hide in production. Admin routes and login are blocked by DisabledInProductionGuard, not only excluded from the OpenAPI spec
What I Learned
- Starting from a personal interest helped me finish the project. Publishing it as a public API still required the same design choices any consumer-facing service would
- Writing the architecture and decision log (AGENTS.md) before and during implementation made trade-offs explicit and kept later decisions consistent with earlier ones
- Serverless deployment surfaces framework assumptions around static file serving and in-memory state that never show up on a traditional always-on server
- Security checklist items from automated review tools need context. Some mattered here, such as unique constraint race conditions. Others did not fit a public, read-heavy API, such as CORS wildcarding
- Personal Access Tokens sharing the Authorization header with JWT are planned for v2. Shipping dual auth before the public read surface was solid would have been premature
Key Features
- Full CRUD across 6 relational resources with many-to-many relationships (characters ↔ elements, weapons, seasons)
- JWT authentication with role-based guards. Admin write operations are disabled in production at the guard level, not only hidden from docs
- Tiered rate limiting (read 200 / write 20 / auth 5 per minute) with per-route throttler overrides
- Consistent {data, meta} success responses and structured error envelopes across the API
- URI-based API versioning so a future v2 can run alongside v1 without breaking existing consumers
- Unit tests with a mocked Drizzle query layer covering service logic and auth. Interactive OpenAPI docs at /docs
Next Project
Minimal E-Shop
Full-stack e-commerce demo with Spring Boot, Stripe payments, and a dockerized backend.