Lesson 4.6: Practice - JSONPlaceholder API
Duration: 75 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Work with a real REST API (JSONPlaceholder)
- Implement CRUD operations in TypeScript
- Handle relationships between resources
- Build a complete API client application
- Apply error handling and loading states
Introduction
In this practice lesson, we will build a complete application that interacts with JSONPlaceholder - a free fake REST API for testing and prototyping. You will apply everything you have learned about HTTP methods, Fetch API, and Axios.
JSONPlaceholder provides these resources:
/posts- 100 posts/comments- 500 comments/users- 10 users/todos- 200 todos/albums- 100 albums/photos- 5000 photos
JSONPlaceholder Overview
Base URL: https://jsonplaceholder.typicode.com
Available Endpoints
| Resource | Endpoint | Methods |
|---|---|---|
| Posts | /posts |
GET, POST |
| Posts | /posts/:id |
GET, PUT, PATCH, DELETE |
| Comments | /comments |
GET, POST |
| Post Comments | /posts/:id/comments |
GET |
| Users | /users |
GET |
| Users | /users/:id |
GET |
| User Posts | /users/:id/posts |
GET |
| Todos | /todos |
GET, POST |
Data Structures
interface Post {
id: number;
userId: number;
title: string;
body: string;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
interface User {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
}
interface Todo {
id: number;
userId: number;
title: string;
completed: boolean;
}
Project: Blog API Client
We will build a TypeScript application that:
- Displays a list of users
- Shows posts by a selected user
- Displays comments on posts
- Allows creating, updating, and deleting posts
Project Structure
blog-client/
├── src/
│ ├── types.ts # Type definitions
│ ├── api.ts # API client
│ ├── utils.ts # Helper functions
│ └── index.ts # Main application
├── package.json
└── tsconfig.json
Step 1: Define Types
// src/types.ts
export interface User {
id: number;
name: string;
username: string;
email: string;
address: Address;
phone: string;
website: string;
company: Company;
}
export interface Address {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
}
export interface Company {
name: string;
catchPhrase: string;
bs: string;
}
export interface Post {
id: number;
userId: number;
title: string;
body: string;
}
export interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
export interface Todo {
id: number;
userId: number;
title: string;
completed: boolean;
}
// Request types (without id, since server assigns it)
export interface CreatePostData {
userId: number;
title: string;
body: string;
}
export interface UpdatePostData {
title?: string;
body?: string;
}
export interface CreateCommentData {
postId: number;
name: string;
email: string;
body: string;
}
Step 2: Build the API Client
// src/api.ts
import {
Comment,
CreateCommentData,
CreatePostData,
Post,
Todo,
UpdatePostData,
User,
} from './types';
const BASE_URL = 'https://jsonplaceholder.typicode.com';
class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
message?: string
) {
super(message || `HTTP Error: ${status} ${statusText}`);
this.name = 'ApiError';
}
}
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = `${BASE_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
throw new ApiError(response.status, response.statusText);
}
// Handle empty responses (DELETE)
const text = await response.text();
return text ? JSON.parse(text) : ({} as T);
}
// ============ Users ============
export async function getUsers(): Promise<User[]> {
return request<User[]>('/users');
}
export async function getUser(id: number): Promise<User> {
return request<User>(`/users/${id}`);
}
// ============ Posts ============
export async function getPosts(): Promise<Post[]> {
return request<Post[]>('/posts');
}
export async function getPost(id: number): Promise<Post> {
return request<Post>(`/posts/${id}`);
}
export async function getUserPosts(userId: number): Promise<Post[]> {
return request<Post[]>(`/users/${userId}/posts`);
}
export async function createPost(data: CreatePostData): Promise<Post> {
return request<Post>('/posts', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updatePost(id: number, data: UpdatePostData): Promise<Post> {
return request<Post>(`/posts/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
export async function deletePost(id: number): Promise<void> {
await request<{}>(`/posts/${id}`, {
method: 'DELETE',
});
}
// ============ Comments ============
export async function getComments(): Promise<Comment[]> {
return request<Comment[]>('/comments');
}
export async function getPostComments(postId: number): Promise<Comment[]> {
return request<Comment[]>(`/posts/${postId}/comments`);
}
export async function createComment(data: CreateCommentData): Promise<Comment> {
return request<Comment>('/comments', {
method: 'POST',
body: JSON.stringify(data),
});
}
// ============ Todos ============
export async function getTodos(): Promise<Todo[]> {
return request<Todo[]>('/todos');
}
export async function getUserTodos(userId: number): Promise<Todo[]> {
return request<Todo[]>(`/users/${userId}/todos`);
}
export async function toggleTodo(id: number, completed: boolean): Promise<Todo> {
return request<Todo>(`/todos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ completed }),
});
}
Step 3: Helper Functions
// src/utils.ts
import { Comment, Post, User } from './types';
export function formatPost(post: Post): string {
return `
[${post.id}] ${post.title}
${'-'.repeat(50)}
${post.body}
`;
}
export function formatComment(comment: Comment): string {
return ` > ${comment.name} (${comment.email})
${comment.body}`;
}
export function formatUser(user: User): string {
return `${user.name} (@${user.username}) - ${user.email}`;
}
export function formatUserDetails(user: User): string {
return `
Name: ${user.name}
Username: @${user.username}
Email: ${user.email}
Phone: ${user.phone}
Website: ${user.website}
Address: ${user.address.street}, ${user.address.suite}, ${user.address.city} ${user.address.zipcode}
Company: ${user.company.name}
`;
}
export function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 3) + '...';
}
export async function measureTime<T>(fn: () => Promise<T>, label: string): Promise<T> {
const start = performance.now();
const result = await fn();
const duration = (performance.now() - start).toFixed(2);
console.log(`${label}: ${duration}ms`);
return result;
}
Step 4: Main Application
// src/index.ts
import * as api from './api';
import { formatComment, formatPost, formatUser, formatUserDetails, measureTime } from './utils';
// ============ Demo Functions ============
async function demoGetAllUsers(): Promise<void> {
console.log('\n=== All Users ===\n');
const users = await measureTime(() => api.getUsers(), 'Fetched users');
users.forEach((user, index) => {
console.log(`${index + 1}. ${formatUser(user)}`);
});
}
async function demoGetUserDetails(userId: number): Promise<void> {
console.log(`\n=== User ${userId} Details ===`);
const user = await api.getUser(userId);
console.log(formatUserDetails(user));
}
async function demoGetUserPosts(userId: number): Promise<void> {
console.log(`\n=== Posts by User ${userId} ===\n`);
const [user, posts] = await Promise.all([api.getUser(userId), api.getUserPosts(userId)]);
console.log(`Posts by ${user.name}:\n`);
posts.slice(0, 3).forEach((post) => {
console.log(formatPost(post));
});
console.log(`... and ${posts.length - 3} more posts`);
}
async function demoGetPostWithComments(postId: number): Promise<void> {
console.log(`\n=== Post ${postId} with Comments ===\n`);
const [post, comments] = await Promise.all([api.getPost(postId), api.getPostComments(postId)]);
console.log(formatPost(post));
console.log(`\nComments (${comments.length}):\n`);
comments.slice(0, 3).forEach((comment) => {
console.log(formatComment(comment));
console.log();
});
}
async function demoCRUDOperations(): Promise<void> {
console.log('\n=== CRUD Operations Demo ===\n');
// CREATE
console.log('1. Creating a new post...');
const newPost = await api.createPost({
userId: 1,
title: 'My New Post',
body: 'This is the content of my new post.',
});
console.log(`Created post with ID: ${newPost.id}`);
console.log(formatPost(newPost));
// READ
console.log('\n2. Reading the post...');
const fetchedPost = await api.getPost(newPost.id);
console.log(`Fetched: ${fetchedPost.title}`);
// UPDATE
console.log('\n3. Updating the post title...');
const updatedPost = await api.updatePost(newPost.id, {
title: 'My Updated Post Title',
});
console.log(`Updated title: ${updatedPost.title}`);
// DELETE
console.log('\n4. Deleting the post...');
await api.deletePost(newPost.id);
console.log('Post deleted successfully!');
}
async function demoParallelRequests(): Promise<void> {
console.log('\n=== Parallel Requests Demo ===\n');
console.log('Fetching multiple users in parallel...');
const userIds = [1, 2, 3, 4, 5];
const users = await measureTime(
() => Promise.all(userIds.map((id) => api.getUser(id))),
'Fetched 5 users in parallel'
);
users.forEach((user) => {
console.log(`- ${formatUser(user)}`);
});
}
async function demoUserDashboard(userId: number): Promise<void> {
console.log(`\n=== Dashboard for User ${userId} ===\n`);
// Fetch all data in parallel
const [user, posts, todos] = await measureTime(
() => Promise.all([api.getUser(userId), api.getUserPosts(userId), api.getUserTodos(userId)]),
'Fetched user dashboard data'
);
console.log(`Welcome, ${user.name}!\n`);
// Posts summary
console.log(`Posts: ${posts.length}`);
console.log('Recent posts:');
posts.slice(0, 3).forEach((post) => {
console.log(` - ${post.title.substring(0, 40)}...`);
});
// Todos summary
const completedTodos = todos.filter((t) => t.completed).length;
console.log(`\nTodos: ${completedTodos}/${todos.length} completed`);
const pendingTodos = todos.filter((t) => !t.completed).slice(0, 3);
console.log('Pending tasks:');
pendingTodos.forEach((todo) => {
console.log(` [ ] ${todo.title.substring(0, 40)}...`);
});
}
async function demoErrorHandling(): Promise<void> {
console.log('\n=== Error Handling Demo ===\n');
// Try to fetch a non-existent user
try {
console.log('Attempting to fetch user 999...');
await api.getUser(999);
} catch (error) {
if (error instanceof Error) {
console.log(`Error caught: ${error.message}`);
}
}
// Try to fetch a non-existent post
try {
console.log('\nAttempting to fetch post 999...');
await api.getPost(999);
} catch (error) {
if (error instanceof Error) {
console.log(`Error caught: ${error.message}`);
}
}
}
// ============ Main Entry Point ============
async function main(): Promise<void> {
console.log('Blog API Client Demo');
console.log('====================');
try {
// Run all demos
await demoGetAllUsers();
await demoGetUserDetails(1);
await demoGetUserPosts(1);
await demoGetPostWithComments(1);
await demoCRUDOperations();
await demoParallelRequests();
await demoUserDashboard(1);
await demoErrorHandling();
console.log('\n=== All demos completed! ===\n');
} catch (error) {
console.error('An error occurred:', error);
}
}
main();
Step 5: Running the Application
# Initialize project
npm init -y
npm install typescript ts-node @types/node
npx tsc --init
# Run the application
npx ts-node src/index.ts
Exercises
Exercise 1: Search Posts
Add a function to search posts by title:
async function searchPosts(query: string): Promise<Post[]> {
// Fetch all posts and filter by title containing query
}
// Test
const results = await searchPosts('qui');
console.log(`Found ${results.length} posts matching "qui"`);
Solution
async function searchPosts(query: string): Promise<Post[]> {
const posts = await api.getPosts();
const lowerQuery = query.toLowerCase();
return posts.filter(
(post) =>
post.title.toLowerCase().includes(lowerQuery) || post.body.toLowerCase().includes(lowerQuery)
);
}
// Test
const results = await searchPosts('qui');
console.log(`Found ${results.length} posts matching "qui"`);
results.slice(0, 5).forEach((post) => {
console.log(`- [${post.id}] ${post.title}`);
});
Exercise 2: User Statistics
Create a function that returns statistics for a user:
interface UserStats {
user: User;
totalPosts: number;
totalTodos: number;
completedTodos: number;
completionRate: number; // percentage
}
async function getUserStats(userId: number): Promise<UserStats> {}
Solution
interface UserStats {
user: User;
totalPosts: number;
totalTodos: number;
completedTodos: number;
completionRate: number;
}
async function getUserStats(userId: number): Promise<UserStats> {
const [user, posts, todos] = await Promise.all([
api.getUser(userId),
api.getUserPosts(userId),
api.getUserTodos(userId),
]);
const completedTodos = todos.filter((t) => t.completed).length;
return {
user,
totalPosts: posts.length,
totalTodos: todos.length,
completedTodos,
completionRate: todos.length > 0 ? Math.round((completedTodos / todos.length) * 100) : 0,
};
}
// Test
const stats = await getUserStats(1);
console.log(`
User: ${stats.user.name}
Posts: ${stats.totalPosts}
Todos: ${stats.completedTodos}/${stats.totalTodos} (${stats.completionRate}%)
`);
Exercise 3: Bulk Operations
Create functions to perform bulk operations:
// Create multiple posts at once
async function createMultiplePosts(posts: CreatePostData[]): Promise<Post[]> {
// Create all posts in parallel
}
// Delete multiple posts
async function deleteMultiplePosts(ids: number[]): Promise<void> {
// Delete all posts in parallel
}
Solution
async function createMultiplePosts(postsData: CreatePostData[]): Promise<Post[]> {
return Promise.all(postsData.map((data) => api.createPost(data)));
}
async function deleteMultiplePosts(ids: number[]): Promise<void> {
await Promise.all(ids.map((id) => api.deletePost(id)));
}
// Test
const newPosts = await createMultiplePosts([
{ userId: 1, title: 'Post 1', body: 'Content 1' },
{ userId: 1, title: 'Post 2', body: 'Content 2' },
{ userId: 1, title: 'Post 3', body: 'Content 3' },
]);
console.log(`Created ${newPosts.length} posts`);
await deleteMultiplePosts(newPosts.map((p) => p.id));
console.log('Deleted all created posts');
Exercise 4: Leaderboard
Create a leaderboard showing users ranked by activity:
interface LeaderboardEntry {
user: User;
postCount: number;
completedTodoCount: number;
score: number; // postCount * 10 + completedTodoCount
}
async function getLeaderboard(): Promise<LeaderboardEntry[]> {
// Get all users and their activity, then rank them
}
Solution
interface LeaderboardEntry {
user: User;
postCount: number;
completedTodoCount: number;
score: number;
}
async function getLeaderboard(): Promise<LeaderboardEntry[]> {
const users = await api.getUsers();
// Fetch posts and todos for all users in parallel
const [allPosts, allTodos] = await Promise.all([api.getPosts(), api.getTodos()]);
const entries: LeaderboardEntry[] = users.map((user) => {
const userPosts = allPosts.filter((p) => p.userId === user.id);
const userTodos = allTodos.filter((t) => t.userId === user.id);
const completedTodos = userTodos.filter((t) => t.completed);
return {
user,
postCount: userPosts.length,
completedTodoCount: completedTodos.length,
score: userPosts.length * 10 + completedTodos.length,
};
});
// Sort by score descending
return entries.sort((a, b) => b.score - a.score);
}
// Test
const leaderboard = await getLeaderboard();
console.log('\n=== Leaderboard ===\n');
leaderboard.forEach((entry, index) => {
console.log(
`${index + 1}. ${entry.user.name} - Score: ${entry.score} ` +
`(${entry.postCount} posts, ${entry.completedTodoCount} todos)`
);
});
Exercise 5: Data Export
Create a function to export user data as a summary:
interface UserExport {
user: User;
posts: Post[];
todos: Todo[];
exportedAt: string;
}
async function exportUserData(userId: number): Promise<UserExport> {}
Solution
interface UserExport {
user: User;
posts: Post[];
todos: Todo[];
exportedAt: string;
}
async function exportUserData(userId: number): Promise<UserExport> {
const [user, posts, todos] = await Promise.all([
api.getUser(userId),
api.getUserPosts(userId),
api.getUserTodos(userId),
]);
return {
user,
posts,
todos,
exportedAt: new Date().toISOString(),
};
}
// Test
const exportData = await exportUserData(1);
console.log('\n=== User Data Export ===\n');
console.log(`User: ${exportData.user.name}`);
console.log(`Posts: ${exportData.posts.length}`);
console.log(`Todos: ${exportData.todos.length}`);
console.log(`Exported at: ${exportData.exportedAt}`);
// Could save to file:
// import { writeFile } from "fs/promises";
// await writeFile("user-export.json", JSON.stringify(exportData, null, 2));
Challenge: Build a CLI Application
As a bonus challenge, build a simple interactive CLI that allows users to:
- List all users
- Select a user and view their posts
- View comments on a post
- Create a new post
- Delete a post
Hint: Use readline module for user input:
import * as readline from 'readline';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function prompt(question: string): Promise<string> {
return new Promise((resolve) => {
rl.question(question, resolve);
});
}
// Usage
const name = await prompt('Enter your name: ');
console.log(`Hello, ${name}!`);
Key Takeaways
- JSONPlaceholder is a free API perfect for learning and prototyping
- Always define types for API responses in TypeScript
- Create reusable API functions for each endpoint
- Use Promise.all for parallel requests when data is independent
- Error handling is essential - always expect requests to fail
- Measure performance to understand API latency
- Helper functions make code more readable
- Build higher-level functions on top of basic API calls
Resources
| Resource | Type | Level |
|---|---|---|
| JSONPlaceholder | API | Beginner |
| JSONPlaceholder Guide | Documentation | Beginner |
| TypeScript Handbook | Documentation | Intermediate |
| Node.js readline | Documentation | Intermediate |
Module Complete
Congratulations! You have completed Module 4: HTTP and REST API. You now know how to:
- Understand what APIs are and how they work
- Use HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Work with headers, query parameters, and request bodies
- Make HTTP requests with Fetch API and Axios
- Handle errors and edge cases
- Build complete applications that interact with APIs
Next Module
Continue your learning journey with Module 5: Working with Data, where you will learn about data transformation, validation with Zod, and more advanced data handling techniques.