When working with APIs in Angular applications, developers often encounter challenges with data fetching. Missing fields, incorrect data types, or overly complex data structures can lead to errors or unnecessary complexity. This article addresses these issues and introduces a robust solution using Zod.js for schema validation and mapping.
Key Takeaways
Common Problems with API Responses:
- Missing mandatory fields or incorrect data types, e.g., an
emailfield that might be absent despite being required. - Overly complex data models with nested structures that complicate usage.
- Missing mandatory fields or incorrect data types, e.g., an
The Solution:
- Validate and parse API responses with Zod.js, a TypeScript-first schema declaration and validation library.
- Use a data service to fetch, validate, and map API responses into application-friendly models.
Implementation Overview
Data Service
The data service fetches data from the API and validates the response using Zod.js:
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs';
import { User } from '../users.model';
import { parseDTO } from './users.dto';
import { fromDTO } from './users.mapper';
@Injectable({
providedIn: 'root',
})
export class UsersDataService {
httpClient = inject(HttpClient);
fetchUsers(): Observable<User[]> {
const url = 'https://dummyjson.com/users';
return this.httpClient.get(url).pipe(
map((response) => {
const dto = parseDTO(response);
if (dto.success) {
return fromDTO(dto.data);
} else {
console.error(dto.error);
return [];
}
}),
catchError((error) => {
console.error(error);
return of([]);
})
);
}
}
Zod.js Schema
import { z } from 'zod';
const usersSchema = z.object({
users: z.array(
z.object({
id: z.number(),
firstName: z.string(),
lastName: z.string(),
age: z.number().optional(),
gender: z.string(),
address: z.object({
address: z.string(),
city: z.string(),
state: z.string(),
}),
company: z.object({
address: z.object({
address: z.string(),
city: z.string().optional(),
state: z.string(),
}),
name: z.string(),
}),
})
),
});
export type UsersDto = z.infer<typeof usersSchema>;
export function parseDTO(source: unknown) {
return usersSchema.safeParse(source);
}
Data mapping
import { join } from 'lodash';
import { User } from '../users.model';
import { UsersDto } from './users.dto';
export function fromDTO(dto: UsersDto): User[] {
return dto.users.map((user) => {
const companyAddress = user.company.address;
const userAddress = user.address;
const fullName = `${user.firstName} ${user.lastName}`;
return {
id: user.id,
fullName,
age: user.age,
gender: user.gender,
company: {
name: user.company.name,
address: join(
[companyAddress.address, companyAddress.city, companyAddress.state],
', '
),
},
address: join(
[userAddress.address, userAddress.city, userAddress.state],
', '
),
};
});
}
Why It Matters
Using this approach ensures:
- Reliability: Your app gracefully handles unexpected API responses.
- Simplicity: Optimized models make templates and business logic straightforward.
- Scalability: The separation of concerns between fetching, parsing, and mapping improves maintainability.
Dive Deeper
The article includes:
- Full implementation of an Angular service for data fetching.
- Examples of Zod.js schemas and type inference.
- A mapping function to transform API responses into application-optimized models.
If you want to make your Angular applications more robust and maintainable, read the full article here.
Stay tuned for tomorrow's advent post on Angular.love for more insights!
