Why I'm Rewriting My Production Laravel API in NestJS
I've spent most of the last 8 years in PHP and Laravel. It's still my go-to for shipping fast — the conventions are sane, Eloquent gets out of your way, and for most CRUD-shaped apps it's hard to beat the speed from idea to working endpoint.
So why am I currently rewriting the backend of Hoogallery — a real, running e-commerce store I built and operate — into NestJS?
Not because Laravel failed me. Because of what I want to build next.
The actual reason: I need an architecture Laravel doesn't give me by default
I've been designing an AI-powered support/sales widget — a RAG-based assistant that Persian e-commerce sellers could install to answer customer questions automatically, pulling from their own product catalog and FAQs instead of giving generic chatbot answers. I want to ship it as a set of reusable, well-typed libraries.
That kind of system leans hard on things that are bolted-on afterthoughts in Laravel but first-class citizens in NestJS:
Background job queues for ingestion pipelines (crawl a site, chunk it, embed it, store it) — Laravel has queues, but BullMQ + NestJS's DI make retry/backoff/monitoring feel native rather than assembled
WebSocket Gateways / SSE for streaming AI responses token-by-token — this is a first-class NestJS primitive, not a package you reach for
Dependency injection as the actual architecture, not an optional pattern — building pluggable adapters (different content sources, different AI providers) is exactly what Nest's module/provider system is designed for
Strict, structured TypeScript end to end — when you're publishing an SDK other people's code will import, types aren't a nice-to-have, they're the interface contract
I could bolt all of this onto Laravel. People do. But I'd be fighting the framework's grain the whole way, and I'd rather learn the tool that's actually built for this shape of problem — properly, not just enough to get something working.
Why Hoogallery, and why now
I didn't want to learn NestJS by building a todo app. I wanted to learn it by rebuilding something with real constraints: actual business rules, actual money math, actual production traffic patterns — even if that traffic is modest, the logic isn't. Hoogallery has genuinely non-trivial pieces: a custom pricing engine for Morse-code-engraved jewelry (more on that in a later post — it's a fun one), an order system with transactional integrity requirements, coupon logic with several interacting condition types.
Rebuilding real logic forces you to confront the differences between frameworks honestly, instead of comparing "hello world" to "hello world."
What I'm actually finding, architecturally
A few observations so far, before I've even finished the migration:
Eloquent's magic vs Prisma's explicitness. Eloquent lets you write $product->features()->attach(...) and trust a lot of implicit behavior. Prisma wants you to be explicit about the relation, the query, the transaction boundary. Slower to write at first, but I've caught myself assuming things about my own Laravel code that turned out to be wrong once I had to write them explicitly in Prisma.
FormRequests vs DTOs are more similar than I expected, structurally — both are "the place validation logic lives, separate from the controller" — but class-validator decorators give you compile-time type safety that a FormRequest's array-based rules never will. You feel this the moment you refactor a field name and the DTO catches every place that breaks, at build time, before you ever hit the endpoint.
Middleware vs Guards is a real conceptual difference, not just syntax. Laravel middleware runs in a pipeline with no inherent awareness of "is this authorization" versus "is this something else." Nest's Guards are specifically, only, about the "can this request proceed" question — narrower, but that narrowness makes intent obvious when you're reading someone else's code six months later.
Sanctum's stateful tokens vs the instinct to reach for JWT. This one surprised me — I expected NestJS's ecosystem to nudge me toward @nestjs/jwt by default, but once I actually thought through my requirements (instant revocation matters for an admin panel; this is one service, not a distributed system), a custom bearer strategy mirroring what Sanctum already does turned out to be the right call, not the JWT default. Framework switch, same architectural conclusion — a good reminder that the tradeoff analysis matters more than the framework's defaults.
## What's next
I'm documenting this migration properly — tests, CI, the real tradeoff reasoning behind each decision — partly because writing it down forces me to actually justify each choice rather than default to whatever's familiar. I'll be posting about specific pieces as I go: the auth strategy decision in more depth, the Morse pricing engine, how I handled patterns Prisma doesn't support natively (Eloquent's soft deletes and polymorphic relations have no direct equivalent).
If you've made a similar jump — Laravel to Nest, or any framework switch driven by "I need different architecture, not different syntax" — I'd genuinely like to hear what surprised you.
Top comments (0)