DEV Community

BeSaRa
BeSaRa

Posted on

Revolutionize Your Angular Development: From Dumb Data to Smart Models with Cast-Response

Tired of fighting with API responses in Angular? Discover how to transform your data handling from a maintenance nightmare to a developer's dream.

πŸ’‘ The Problem We All Face

If you've worked with Angular and APIs, you've likely encountered this familiar frustration:


// ❌ The current reality - dumb data containers
interface User {
  id: number;
  firstName: string;
  lastName: string;
  createdAt: string; // Wait, why is this a string?
  birthDate: string;  // And this too?
  status: string;
}

// ❌ Business logic scattered everywhere
@Component({
  template: `
    <div>
      {{ getUserFullName(user) }}
      {{ isUserActive(user) }}
      {{ calculateUserAge(user) }}
    </div>
  `
})
export class UserComponent {
  // Why is this logic in my component?
  getUserFullName(user: User): string {
    return `${user.firstName} ${user.lastName}`;
  }

  isUserActive(user: User): boolean {
    return user.status === 'active';
  }

  calculateUserAge(user: User): number {
    return new Date().getFullYear() - new Date(user.birthDate).getFullYear();
  }
}

Enter fullscreen mode Exit fullscreen mode

Sound familiar? You're not alone. This approach leads to:

  • Business logic scattered across components and services
  • Manual data transformation for every API call
  • No runtime type safety - TypeScript only checks at compile time
  • Inconsistent patterns across different services
  • Maintenance nightmares when API contracts change

🎯 The Solution: Cast-Response

What if I told you there's a better way? A way where your models are smart, your business logic is encapsulated, and data transformation happens automatically?

Meet Cast-Response - an Angular library that transforms how you handle API responses.

✨ Transform Dumb Data into Smart Models


// βœ… Smart models with encapsulated business logic
class User {
  id!: number;
  firstName!: string;
  lastName!: string;
  email!: string;
  createdAt!: Date;    // Real Date objects! note to make it happend check the part of model interceptor 
  birthDate!: Date;
  status!: 'active' | 'inactive' | 'pending';

  // βœ… Computed properties
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  get initials(): string {
    return `${this.firstName.charAt(0)}${this.lastName.charAt(0)}`.toUpperCase();
  }

  get age(): number {
    return new Date().getFullYear() - this.birthDate.getFullYear();
  }

  // βœ… Business logic methods
  isActive(): boolean {
    return this.status === 'active';
  }

  canDelete(): boolean {
    return this.isActive() && this.isRecent();
  }

  getGreeting(): string {
    const hour = new Date().getHours();
    if (hour < 12) return `Good morning, ${this.firstName}!`;
    if (hour < 18) return `Good afternoon, ${this.firstName}!`;
    return `Good evening, ${this.firstName}!`;
  }

  private isRecent(): boolean {
    const sevenDaysAgo = new Date();
    sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
    return this.createdAt > sevenDaysAgo;
  }
}

Enter fullscreen mode Exit fullscreen mode

🎩 Automatic Magic with Zero Boilerplate

The real magic happens with the @CastResponse decorator:


@Injectable()
export class UserService {
  private http = inject(HttpClient);

  // βœ… Automatic casting - zero boilerplate!
  @CastResponse(() => User)
  getUser(id: number): Observable<User> {
    return this.http.get(`/users/${id}`);
  }

  // βœ… Automatic array casting, it's checking if the returned response array and it will cast each item to User
  @CastResponse(() => User)
  getAllUsers(): Observable<User[]> {
    return this.http.get('/users');
  }

  // βœ… Complex nested casting
  @CastResponse(() => User, {
    shape: {
      profile: () => Profile,
      'posts.*.author': () => User,
      'posts.*.comments.*': () => Comment
    }
  })
  getUserWithDetails(id: number): Observable<User> {
    return this.http.get(`/users/${id}?include=profile,posts,comments`);
  }
}

Enter fullscreen mode Exit fullscreen mode

Yes, it's really that simple! Your API responses are automatically transformed into real class instances with all methods and computed properties available immediately.

πŸ—οΈ Enterprise-Ready Patterns

Base CRUD Services Made Practical
One of the most powerful features? Generic base services that actually work:


// βœ… Base CRUD service that works with any model
export abstract class BaseCrudService<T> {
  protected abstract endpoint: string;
  protected http = inject(HttpClient);

  @CastResponse(undefined, { fallback: '$default' })
  findById(id: number): Observable<T> {
    return this.http.get(`${this.endpoint}/${id}`);
  }

  @HasInterception
  @CastResponse(undefined, { fallback: '$default' })
  create(@InterceptParam data: T): Observable<T> {
    return this.http.post(this.endpoint, data);
  }

  @CastResponse(undefined, { fallback: '$default' })
  findAll(): Observable<T[]> {
    return this.http.get(this.endpoint);
  }
}

// βœ… Clean, focused service implementations
@CastResponseContainer({
  $default: { 
    model: () => User, 
    shape: { profile: () => Profile } 
  }
})
export class UserService extends BaseCrudService<User> {
  protected endpoint = '/users';
  // That's it! All CRUD operations inherited
}

@CastResponseContainer({
  $default: { 
    model: () => Product, 
    shape: { category: () => Category } 
  }
})
export class ProductService extends BaseCrudService<Product> {
  protected endpoint = '/products';
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ Bi-directional Interceptors

Handle data transformation both ways with powerful interceptors:

export class UserInterceptor implements InterceptorContract<User> {
  // βœ… Transform incoming data from API
  receive(user: User): User {
    user.fullName = `${user.firstName} ${user.lastName}`;
    user.createdAt = new Date(user.createdAt);
    user.birthDate = new Date(user.birthDate);
    return user;
  }

  // βœ… Transform outgoing data to API
  send(user: Partial<User>): Partial<User> {
    const { fullName, age, ...cleanUser } = user;
    cleanUser.createdAt = cleanUser.createdAt.toISOString();
    return cleanUser;
  }
}

// Attach to your model
@InterceptModel(new UserInterceptor())
class User {
  // Your smart model class
}
Enter fullscreen mode Exit fullscreen mode

🌟 Real-World Benefits

In Your Components

Before

// ❌ Logic scattered everywhere
@Component({
  template: `
    <div>
      {{ getUserFullName(user) }}
      {{ isUserActive(user) }}
      {{ calculateUserAge(user) }}
      <button [disabled]="!canDeleteUser(user)">Delete</button>
    </div>
  `
})

Enter fullscreen mode Exit fullscreen mode

After

// βœ… Clean, focused components
@Component({
  template: `
    <div>
      {{ user.fullName }}
      {{ user.isActive() }}
      {{ user.age }}
      <button [disabled]="!user.canDelete()">Delete</button>
      <p>{{ user.getGreeting() }}</p>
    </div>
  `
})
Enter fullscreen mode Exit fullscreen mode

Maintenance & Refactoring

Scenario: Your API changes date format from string to timestamp.

Before Cast-Response: Update every transformation function across your entire application.

With Cast-Response: Update one interceptor. Done.

πŸš€ Advanced Features That Will Blow Your Mind

Wildcard Support for Complex Data Structures

@CastResponse(() => Dashboard, {
  shape: {
    'users.*.profile': () => Profile,        // Nested user profiles
    'settings.{}': () => Setting,            // Dynamic object properties
    'users.*.comments.*.author': () => User  // Multiple levels deep
  }
})
getDashboard(): Observable<Dashboard> {
  return this.http.get('/dashboard');
}
Enter fullscreen mode Exit fullscreen mode

Response Unwrapping

// For APIs that wrap responses: { data: { user: {...} }, status: 'success' }
@CastResponse(() => User, { unwrap: 'data.user' })
getUser(id: number): Observable<User> {
  return this.http.get(`/api/users/${id}`);
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Š The Impact: Before vs After

Aspect Before Cast-Response With Cast-Response
Business Logic Scattered everywhere Encapsulated in models
Data Transformation Manual, repetitive Automatic, zero effort
Type Safety Compile-time only Runtime guaranteed
Code Maintenance High complexity Simple, centralized
Developer Experience Frustrating, error-prone Joyful, productive
Service Consistency Every team does it differently Standardized patterns

Who Is This For?

  • Angular Teams tired of manual data transformation
  • Enterprise Applications needing consistent patterns
  • Full-Stack Developers wanting better type safety
  • Tech Leads looking to improve code quality
  • Anyone who believes their data should be smart, not dumb

🏁 Getting Started

npm install cast-response
Enter fullscreen mode Exit fullscreen mode

Basic setup:

// 1. Create your smart model
export class User {
  id!: number;
  name!: string;
  createdAt!: Date;

  get displayName(): string {
    return `User: ${this.name}`;
  }
}

// 2. Use in your service
@Injectable()
export class UserService {
  @CastResponse(() => User)
  getUser(id: number): Observable<User> {
    return this.http.get(`/users/${id}`);
  }
}

// 3. Enjoy smart models in your components!
user$.subscribe(user => {
  console.log(user.displayName);      // "User: John Doe"
});
Enter fullscreen mode Exit fullscreen mode

πŸ’­ The Philosophy Behind Cast-Response

For too long, we've treated frontend data as second-class citizens. We create interfaces that are nothing more than type definitions, then scatter business logic throughout our applications.

Cast-Response challenges this paradigm. It brings true object-oriented principles to your Angular data layer:

Encapsulation: Business logic lives with the data it operates on

Abstraction: Complex transformations happen automatically

Consistency: Standardized patterns across your entire application

Safety: Runtime type checking prevents unexpected errors

πŸ€” Ready to Transform Your Angular Development?

Cast-Response isn't just another library - it's a fundamental shift in how we think about data in Angular applications. It's the result of 3 years of battle-testing in production, solving real problems that every Angular developer faces.

The question isn't whether you need better data handling - it's whether you're ready to stop accepting the status quo.

πŸ”— Resources:

  • πŸ“¦ NPM: npm install cast-response
  • πŸ™ GitHub: Repository
  • πŸŽ₯ Video Tutorial (in arabic as of now): Video Tutorial

πŸ’¬ Join the Conversation

I'd love to hear your thoughts!

  • Have you faced similar challenges with Angular data handling?
  • What patterns have worked (or failed) for your team?
  • How do you see Cast-Response fitting into your workflow?
  • Let's build better Angular applications, together! πŸš€
  • What feature excites you the most? Share in the comments below! πŸ‘‡

Top comments (0)