Part 2.1 — Setting Up HTTP Fetching in React with TypeScript
Most real applications become useful only after they talk to a server. Whether you’re loading a task list, creating users, or updating a profile, everything flows through HTTP requests.
In this post, we’ll set up clean, type-safe HTTP fetching patterns using React + TypeScript — the same patterns you’ll rely on when connecting to your NestJS backend.
1. Why Use Fetch (and Not Axios)?
React doesn’t require any special library for HTTP requests.fetch is:
built into the browser
fully typed in TypeScript
Promise-based
flexible enough for REST patterns
Later, when we add authentication, pagination, or error handlers, fetch stays predictable.
2. A Minimal Fetch Example (the Right Way)
Let’s start with a simple component that loads users from a REST endpoint:
import { useEffect, useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
export function UsersList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
async function load() {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed to load users');
const data = (await res.json()) as User[];
setUsers(data);
setLoading(false);
}
load();
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}This is the simplest form of data fetching: load on mount, set state, render.
But we can do better.
3. Move Fetching Into a Reusable Function
The component shouldn’t know how your API works — just what it returns.
Let’s create a typed fetch utility.
// src/lib/http.ts
export async function http<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const message = await res.text();
throw new Error(message || `HTTP error: ${res.status}`);
}
return res.json() as Promise<T>;
}Now fetching users becomes:
import { http } from '../lib/http';
import type { User } from '../types/user';useEffect(() => {
http<User[]>('/api/users')
.then(data => setUsers(data))
.catch(console.error)
.finally(() => setLoading(false));
}, []);This will scale beautifully when you add API keys, JWT auth, or retry logic.
4. Building a Use-Users Hook (Cleaner UI)
Components should focus on markup, not side-effects.
Let’s create a custom hook for fetching users.
// src/hooks/useUsers.ts
import { useEffect, useState } from 'react';
import { http } from '../lib/http';
import type { User } from '../types/user';
export function useUsers() {
const [data, setData] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
http<User[]>('/api/users')
.then(setData)
.finally(() => setLoading(false));
}, []);
return { data, loading };
}Then the UI becomes trivial:
// Component
const { data: users, loading } = useUsers();
if (loading) return <p>Loading...</p>;
return users.map(u => <p key={u.id}>{u.name}</p>);Small, readable, and testable.
5. Posting Data (Creating New Records)
Fetching isn’t just reads — you’ll soon do CREATE operations.
Let’s build a typed POST helper:
// src/api/users.ts
import { http } from '../lib/http';
import type { User } from '../types/user';
interface CreateUserDto {
name: string;
email: string;
}
export function createUser(dto: CreateUserDto) {
return http<User>('/api/users', {
method: 'POST',
body: JSON.stringify(dto),
});
}Usage in a form handler:
async function handleSubmit() {
try {
const user = await createUser({ name, email });
setUsers(prev => [...prev, user]);
} catch (err) {
console.error(err);
}
}Notice that frontend DTOs match backend DTOs — once we add NestJS controllers in Part 3, everything aligns.
6. Updating Data (PUT/PATCH)
Same pattern, different method:
export function updateUser(id: number, dto: Partial<User>) {
return http<User>(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(dto),
});
}On the UI side, updating becomes natural:
await updateUser(user.id, { name: 'Updated Name' });7. Deleting Data
REST is straightforward:
export function deleteUser(id: number) {
return http<void>(`/api/users/${id}`, { method: 'DELETE' });
}Your component simply filters out the deleted record.
8. Why Shared Types Matter
When React, NestJS, and Prisma all share a User definition (via /packages/types), you eliminate entire classes of bugs:
Mismatched field names
Outdated responses
Incorrect client expectations
Later, when the backend changes, TypeScript forces your frontend to update too.
That’s full-stack correctness with minimal friction.
9. Production Considerations (You’ll Use These Soon)
As your REST calls grow, you’ll want:
Cancellation for slow requests
Retry logic
Exponential backoff
Global error boundaries
JWT token injection
Request logging for debugging
We’ll weave these into upcoming parts once your NestJS API is live.
10. Summary
You now know how to:
Use
fetchwith TypeScript typesBuild reusable HTTP utilities
Fetch and display data with custom React hooks
Handle POST, PATCH, and DELETE operations cleanly
Prepare your frontend for real REST integration
In Part 2.2, we’ll build complete CRUD interfaces and align them with API contracts — shaping the frontend around the schema your NestJS backend will expose.
Related
Part 2.5 — Consuming REST APIs with Shared Types & Error Handling Strategies
Real-world frontends need safety: typed API responses, predictable errors, and consistent client logic. This post teaches you the patterns professionals use to integrate React with REST cleanly.
Part 2.4 — UX State: Loading, Error, and Empty States in React
A React app becomes truly usable when it handles all states gracefully: loading, error, empty, and success. In this post, you’ll learn the UX patterns professionals rely on.
Part 2.3 — Handling Forms, Validation & Submission to REST Endpoints
Forms connect users to data. In this post, you’ll learn how to validate input, manage UX states, and submit cleanly typed data to real REST endpoints.
Comments