You need more than a login endpoint to secure a REST API. Next.js JWT authentication only blocks anonymous traffic; real systems also need role checks, attribute conditions, and per-record permissions. This guide rebuilds the Full Cycle live tutorial layer by layer so you can see where each decision belongs.
What Does the Next.js JWT Authentication Tutorial Build?
Next.js JWT authentication and authorization are demonstrated end to end in a three-hour live coding session published by the Full Cycle channel in April 2025. The instructor builds a small REST API for posts and users: users register in a PostgreSQL database, log in to receive a signed JWT, and then read or modify posts only when their role and attributes allow it.
Next.js is a Node.js framework created in 2017 that organizes code into modules, controllers, and injectable services, with decorators driving routing and dependency injection. The tutorial uses version 11, which the framework had recently released at the time of recording. Developers coming from Java and Spring usually recognize the structure immediately.
The stated purpose of the live session was preparation for an April 2025 full-stack immersion event, so the API is intentionally minimal. The value is in the security layers, not in the business logic: the instructor explicitly warns that authorization, not authentication, is where most projects accumulate unmanageable hardcoded rules.
How Is the Project Set Up with the Nest CLI, Docker, and Prisma?
The setup takes four tools, each responsible for one layer of the stack. The Nest CLI scaffolds code artifacts, Docker runs the database, Prisma maps the schema, and VS Code edits the TypeScript source.
- Install Node through nvm, then run
nest newto create a project of roughly four source files:main.ts,app.module.ts, a controller, and a service. - Run
nest generate resourceto scaffold a full REST CRUD module. The generator creates the controller, service, DTOs, and module, and registers them automatically. - Start PostgreSQL with a short
docker-compose.ymlexposing port 5432, and point the.envfile atlocalhost:5432. - Run
npx prisma init, model aUserwith a unique email, a required password, and aRolesenum (ADMIN,READER,EDITOR,WRITER) defaulting toREADER, then apply the schema withnpx prisma migrate dev. - Create a
PrismaServicethat extendsPrismaClientand connects on module init, registered as a@Global()module so every other module can inject it.
The global-module step is where beginners usually stall. A Nest module is private by default: one module cannot use another module's providers unless they are explicitly exported. Marking the Prisma module global removes the need to import it everywhere, and the same trick later applies to the JWT module, which must be registered with global: true so guards in other modules can inject JwtService.
How Does the Login Flow Issue a JWT?
The login endpoint verifies credentials and returns a signed token in three steps: look up the user, compare the password hash, and sign the payload. Registration already stores passwords safely, because the service hashes them with bcrypt before writing to the database. The salt guarantees that two users with the same plaintext password produce different hashes, which defeats rainbow-table and brute-force shortcuts against naive hash functions.
POST /auth/loginreceives a DTO with email and password through the@Body()decorator.- The service queries Prisma for the user by email and returns a not-found error if the row is missing.
bcrypt.comparechecks the plaintext password against the stored hash. On failure the API returns a generic invalid-credentials error, deliberately identical to the not-found path so attackers cannot discover which emails exist.- On success,
JwtService.signAsyncreturns an access token containing the user name, email, role, and the standardsubclaim set to the user id.
The JWT module is configured with an explicit secret, a two-hour expiry, and the HS256 signing algorithm. Pinning the algorithm matters: the instructor points out that algorithm-confusion attacks such as RS256-to-HS256 exploits target implementations that accept whatever alg header a token claims. After login, pasting the token into jwt.io shows the decoded payload, including the exp and sub standard claims.
One TypeScript detail is worth copying: DTOs are classes, not interfaces. Interfaces and types are erased at compile time, so runtime validation decorators cannot attach to them. Classes survive into the compiled JavaScript, which is what makes request validation and data filtering possible.
How Does an Auth Guard Protect Routes?
A guard implements the canActivate method and decides whether a request may proceed, working for HTTP, message brokers, or any other execution context. The auth guard in the tutorial extracts the Bearer token from the Authorization header, verifies it with JwtService.verifyAsync restricted to HS256, loads the user from the database by the sub claim, and attaches the user to the Express request object.
Attaching the user requires a small TypeScript augmentation: Express's Request type has no user property, so the project adds a declare global namespace merge in a .d.ts file that imports the Prisma User type. After that, controllers receive the authenticated user through @Req() with full type safety.
Applying @UseGuards(AuthGuard) at the controller level protects every route in it, while placing the decorator on a single method leaves the rest public. Guards run in order, which matters later: the authentication guard must run before the authorization guard, because you cannot authorize an unidentified user.
How Does Role-Based Access Control (RBAC) Work Here?
RBAC, role-based access control, answers the question 'which roles may perform this action?' with three agents: the subject (user or system), the role, and the permission. The tutorial first implements it the way the official Nest documentation shows, using a custom decorator and a second guard.
- A
@Roles()decorator built onSetMetadatarecords the allowed roles for each handler, for example@Roles(Roles.WRITER, Roles.EDITOR)on post creation. - A
RolesGuarduses Nest'sReflectorservice to read that metadata from the executing handler. - If no roles are required the guard returns true; otherwise it reads the authenticated user from the request and allows access when the user is an admin or when the user's role appears in the required list.
Then the instructor breaks his own example, and this is the most instructive moment in the video. A reader may read posts, but only published ones. A writer may edit posts, but only his own. Expressing those rules in RBAC means either adding conditional code inside controllers and services, or inventing endless new roles. Both paths scatter authorization logic across the codebase, and a simple requirement change then forces edits in many uncontrolled places, with real risk of over-granting access.
The lesson generalizes beyond this codebase: the roles-to-permissions mapping changes over time, so decorators should reference stable permissions such as create:post rather than raw roles, and the role-to-permission relationship should live in exactly one place.
Why Move from RBAC to Attribute-Based Control with CASL?
ABAC, attribute-based access control, evaluates attributes of the resource and the context of the request instead of only the user's role. A doctor may read patient records only during working hours; a reader may read only published posts; a writer may edit only his own articles. CASL is a JavaScript authorization library that implements RBAC, ABAC, and ACL with one API, which is why the tutorial adopts it.
The integration defines three types: PermissionActions (including CASL's built-in manage, which grants every action on a resource), Subjects mapping resource names to Prisma models, and an AppAbility built from the Ability pair type. A definePermissions function receives a role and an AbilityBuilder and chains can calls, such as can(AbilityAction.read, AbilitySubject.Post, { published: true }) for readers or { authorId: userId } for writers. The CaslAbilityService that wraps this map is request-scoped (Scope.REQUEST), so each request gets a freshly built ability object.
The decisive trick is pushing conditions into the database query instead of fetching and filtering in memory. With @casl/prisma, the service passes accessibleBy(ability).Post into Prisma's where Claude, so a reader's query gains WHERE published = true and a writer's gains WHERE authorId = ..., while an admin's query gains nothing. The instructor logs Prisma queries during the demo to prove the generated SQL changes per role. Querying all posts and filtering in application code would work but would waste database work on rows the user can never see.
This design also changes error semantics. A reader requesting an unpublished post receives a 404, not a 403, because the resource is outside his visibility context entirely. Revealing that a forbidden post exists through a 403 leaks information; returning 404 keeps the API's language consistent. For record-level grants such as sharing one specific file or post with one user, the same dynamic permission field doubles as an ACL, the model found in file systems, Google Drive, and Amazon S3.
RBAC vs ABAC vs ACL: Which Model Fits Which Rule?
The three authorization models are complementary, not competing, and the tutorial ends by using all three at once. The table below summarizes how each was applied in the codebase.
| Model | Question it answers | Tutorial example | Best fit |
|---|---|---|---|
| RBAC | Which roles may act? | @Roles(WRITER, EDITOR) on create post | Coarse, stable role rules |
| ABAC | Which resource attributes allow it? | Reader sees only published: true posts | Rules depending on resource state or context |
| ACL | Who may touch this exact record? | Per-user permission grants stored as JSON | Sharing one file, post, or record |
Permissions are also stored per user as a JSON column in Prisma, typed with @prisma/-types so the action, resource, and condition fields autocomplete. At request time the service hydrates the ability builder from both the role map and any user-specific permissions, letting an administrator grant a single writer read access to the users resource without touching code.
The instructor's closing argument is that knowing the models matters more than knowing the library. If your authorization knowledge is tied to one API, every framework or language change resets you to zero; if you understand RBAC, ABAC, and ACL, the library is interchangeable.
What Are the Trade-offs of Loading Permissions from the Token?
The tutorial demonstrates one optional optimization and names its cost directly. Because the JWT is signed and its payload cannot be modified without invalidating it, the login endpoint can embed CASL packRules output in the token, and the guard can hydrate the ability service from the payload instead of rebuilding permissions from the database on every request.
Faster per-request handling is the benefit. The cost is size: the token travels with every request, so stuffing many permission rules into the payload inflates network latency on every call. For the same reason, the instructor notes that large production systems rarely support fully dynamic role creation in the database; roles are usually hardcoded in the permission map, and anything truly dynamic is cached or, when the token grows too large, moved to a cache keyed by user rather than carried in the request.
There is no definitive choice, and the video does not pretend there is one. The recommendation is to know the fundamentals, measure where your requests actually spend time, and place authorization where it matches your change frequency: generic rules in code, user-specific grants in the database, and only well-understood hot paths in the token.
Frequently Asked Questions
- Why pin the HS256 algorithm when signing JWTs in Next.js? Pinning prevents algorithm-confusion attacks where an attacker switches the token's
algheader to bypass verification. The tutorial sets the algorithm explicitly inJwtModule.registerand again when verifying in the guard.
- Why use classes instead of interfaces for Next.js DTOs? Interfaces and type aliases are erased when TypeScript compiles to JavaScript, leaving nothing for runtime validation to inspect. Classes persist at runtime, so validation decorators and data filtering can work on them.
- Why return 404 instead of 403 for forbidden resources? When a resource is filtered out of a user's queries by attribute rules, it does not exist in that user's context. A 403 would confirm the resource exists, leaking information the user should not have.
- Can CASL work with ORMs other than Prisma? Yes. Native query integration ships for Prisma via @casl/prisma, and the instructor notes he has shown equivalent integration with TypeORM in his paid course; the adapter approach generalizes to other ORMs.
- Should permissions be stored in the JWT payload? Only when the rule set is small and request latency matters, because the token is sent with every call and larger payloads raise network overhead. Larger or faster-changing permission sets belong in the database or a cache.
Turn Your Own Walkthroughs into Articles
This article exists because a single three-hour live session contained a complete argument about authorization models, salt hashing, guard ordering, and query-level permissions. If you have recorded similar material, long explanations, interviews, or lessons that live only inside a YouTube video, the same content can become a searchable written reference. Visit Skala Blog, paste a YouTube URL, and generate an article from the transcript, so the knowledge in your recordings works as documentation too.
Materials covering similar Next.js auth ground include Crazystack Typescript and the Bootcamp do Dev Doido by Gustavo Dev Doido, both of which walk through authentication and authorization flows in TypeScript projects.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits