ecommerce-nestjs-backend
overview
A production grade GraphQL API built with NestJS for an e-commerce platform. The core challenge was designing a clean, type-safe authorization system where User and Admin roles could access the same endpoints with distinct permission scopes without duplicating resolvers or burying brittle if/else checks deep in service logic.
The solution was a composable guard pipeline: an AuthGuard validates the JWT and attaches the decoded user to the request context, then a RolesGuard reads the @Roles() decorator metadata via NestJS's Reflector. Role requirements live at the resolver declaration not scattered through service methods. Prisma ORM was chosen over TypeORM for its stronger type inference and migration tooling: schema changes produce auto-generated, fully-typed client code, eliminating the ORM/DB drift that causes runtime bugs in large codebases.
The GraphQL schema-first approach means the API contract is explicit, versioned, and self-documenting. The dual-tier RBAC ensures Admin operations inventory management, order fulfilment, user administration are unreachable from User-scoped tokens at the API boundary, not just hidden in the UI.
architecture
Client Request
│
▼
GraphQL Gateway (NestJS)
│
├──► AuthGuard
│ └──► JWT Strategy (Passport)
│ validates token, attaches user
│
├──► RolesGuard
│ └──► Reflector
│ reads @Roles() metadata
│
▼
Resolver Layer
├── ProductResolver @Roles(User)
├── OrderResolver @Roles(User, Admin)
└── AdminResolver @Roles(Admin)
│
▼
Service Layer (business logic)
│
▼
PrismaService
│
▼
PostgreSQLGuards execute in the order they are listed in @UseGuards(). The AuthGuard runs first if the JWT is invalid, the request is rejected before any role check happens. The RolesGuard then reads the @Roles() metadata attached to the resolver via NestJS's Reflector, compares it against the user object on the request, and either allows or denies access. This composition pattern keeps authorization cross-cutting without polluting business logic.
technical.decisions
GraphQL provides a strongly typed schema as the explicit API contract, reducing over-fetching and enabling frontend teams to compose queries without backend changes. NestJS code-first approach generates the schema from TypeScript decorators a single source of truth for both the type system and the API surface.
Prisma's type inference is significantly stronger the generated client reflects exact column types including nullability, without requiring manual synchronization between entity classes and DB schema. The migration system produces auditable SQL diffs. TypeORM's decorator-heavy approach couples entity classes to schema in a way Prisma deliberately avoids.
Rather than calling role-check logic inside each service method, guards are applied at the resolver level using @UseGuards() and @Roles() decorators. NestJS's Reflector reads metadata at runtime. This makes permission requirements visible in the resolver declaration and keeps authorization at the API boundary.
User and Admin scopes are enforced independently rather than as a hierarchy. An Admin token cannot automatically access User-only operations like a user's private order history the separation is explicit in the resolver map. This prevents the privilege escalation patterns that are common in role-hierarchy implementations.
outcomes
- Role separation enforced at the API boundary not in UI conditionals
- Schema-first GraphQL contract shared across all consumers
- Prisma schema as single source of truth no ORM/DB type drift
- Guard composition visible at resolver declaration level
- Dual-tier RBAC prevents privilege escalation by design