Isomorphic Authorization with CASL: The Ultimate Trick to Sync NestJS and React Logic

Development tutorial - IT technology blog
Development tutorial - IT technology blog

The “Duplicated Logic” Trap in Authorization

Have you ever been in this situation? You write a bunch of if (user.role === 'admin') logic in NestJS to protect your API. Immediately after, you have to copy that exact logic to React to show or hide buttons. Everything slowly turns into a nightmare when requirements change. Suppose your boss wants an ‘Editor’ to also be able to delete posts; you’ll have to dig through both ends of the project to fix it. Just missing one spot can lead to logic bugs or, more seriously, security vulnerabilities.

That’s where Isomorphic Authorization comes to the rescue. Instead of maintaining logic in two places, we define a single set of rules. CASL is the most powerful library to make this a reality.

Why CASL is a Game Changer

CASL (pronounced /’kæsəl/) manages authorization based on abilities. Instead of rigid Role checks like isAdmin, CASL focuses on: what actions a user is allowed to perform on which subjects.

The biggest advantage of CASL is its consistency. You define the logic in a shared TypeScript file, then import it into NestJS to block APIs and into React to render the UI. When the logic changes, you only need to edit one single file.

4 Concepts You Need to Master

  • Actions: Actions like manage (full access), create, read, update, delete.
  • Subject: The target object such as User, Post, or 'all'.
  • Ability: A collection of rules allowing user operations.
  • Conditions: Deeper conditions. For example: A user can only edit a post if the authorId matches their userId.

Hands-on: Implementing CASL for a Fullstack Project

1. Setting Up Shared Logic

Create an ability.ts file in the shared folder. This will be the “single source of truth” for both the Backend and Frontend.

// shared/ability.ts
import { AbilityBuilder, PureAbility, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability';

export enum Action {
  Manage = 'manage',
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

export type Subjects = InferSubjects<'Post' | 'User'> | 'all';
export type AppAbility = PureAbility<[Action, Subjects]>;
export const AppAbility = PureAbility as AbilityClass<AppAbility>;

export function defineAbilityFor(user: any) {
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(AppAbility);

  if (user.role === 'admin') {
    can(Action.Manage, 'all'); 
  } else {
    can(Action.Read, 'all');
    // Real-world rule: Only allow editing own posts
    can(Action.Update, 'Post', { authorId: user.id }); 
    cannot(Action.Delete, 'Post').because('Only administrators have permission to delete posts');
  }

  return build({
    detectSubjectType: (item) => item.constructor.name as ExtractSubjectType<Subjects>,
  });
}

2. Integrating into NestJS Backend

On the Backend, we need a Factory to initialize permissions based on the User from the Request. When handling complex authorization objects containing lots of metadata, I often use toolcraft.app to format and check JSON structures faster, avoiding logic confusion.

npm install @casl/ability

Create a CaslAbilityFactory to use in Guards:

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: any) {
    return defineAbilityFor(user);
  }
}

After that, you just need to write a PermissionsGuard to check permissions before the Request reaches the Controller. This helps block unauthorized access right at the first layer of defense.

3. Synchronizing with React Frontend

Install the React support package:

npm install @casl/react @casl/ability

Use AbilityContext to wrap your application. You will reuse the exact defineAbilityFor function from step 1. At this point, showing or hiding the UI becomes effortless:

import { Can } from './context/AbilityContext';

function PostItem({ post }) {
  return (
    <div>
      <h2>{post.title}</h2>
      {/* Edit button only appears if the user has permission to update this specific post */}
      <Can I="update" this={post}>
        <button>Edit</button>
      </Can>

      <Can I="delete" a="Post">
        <button>Delete</button>
      </Can>
    </div>
  );
}

Pro-tip: Don’t Lose Track of Classes

A common mistake is when the Backend returns raw JSON; CASL on the Frontend won’t know which Class that Object belongs to. As a result, rules like a('Post') will fail.

The solution is to use a helper function to “hydrate” the object. You can assign a __type property or use class-transformer to convert raw data into Class instances. Then, CASL will accurately map the can/cannot rules you’ve defined.

Summary

Applying CASL helps your code follow the DRY (Don’t Repeat Yourself) principle. Although it takes a bit of time for the initial setup, you’ll save hours of debugging later—especially as the project scales to dozens of Roles and hundreds of permissions. Don’t forget to use toolcraft.app to help inspect authorization data when needed. Good luck with your implementation!

Share: