Working with External APIs
REST API Patterns
A REST API (Representational State Transfer Application Programming Interface) is the most common style of web API. It uses HTTP methods to perform actions on resources:
| HTTP Method | Action | Example |
|---|---|---|
| GET | Retrieve data | GET /posts — get all posts |
| POST | Create new data | POST /posts — create a post |
| PUT/PATCH | Update data | PATCH /posts/1 — update post 1 |
| DELETE | Remove data | DELETE /posts/1 — delete post 1 |
The API responds with JSON (JavaScript Object Notation) data and an HTTP status code indicating success or failure.
API Keys and Environment Variables
Many APIs require an API key to authenticate your requests. Never hardcode keys in your source code — use environment variables:
# .env.local — never commit this file
VITE_WEATHER_API_KEY=abc123yoursecretkey
VITE_NEWS_API_KEY=xyz789anothersecret
// src/config.js
export const config = {
weatherApiKey: import.meta.env.VITE_WEATHER_API_KEY,
weatherBaseUrl: "https://api.openweathermap.org/data/2.5"
};
Building a Custom useFetch Hook
A custom hook extracts reusable logic from components. The useFetch hook is one of the most useful patterns in React:
// src/hooks/useFetch.js
import { useState, useEffect } from "react";
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
let cancelled = false; // Prevent state updates after unmount
setLoading(true);
setError(null);
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.json();
})
.then(json => {
if (!cancelled) {
setData(json);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
});
return () => {
cancelled = true; // Cleanup: cancel stale updates
};
}, [url]);
return { data, loading, error };
}
Using the hook in a component is clean and declarative:
function UsersList() {
const { data: users, loading, error } = useFetch(
"https://jsonplaceholder.typicode.com/users"
);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Loading, Error, and Success States
Every API call should handle three distinct UI states:
function ApiComponent() {
const { data, loading, error } = useFetch("/api/data");
// 1. Loading state
if (loading) {
return (
<div className="loading-container">
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
// 2. Error state
if (error) {
return (
<div className="error-container">
<h3>Something went wrong</h3>
<p>{error}</p>
<button onClick={() => window.location.reload()}>Try Again</button>
</div>
);
}
// 3. Success state
return <div>{/* Render data */}</div>;
}
Rate Limiting and CORS
Rate Limiting
Most APIs limit how many requests you can make per minute or day. Handle 429 (Too Many Requests) responses:
if (response.status === 429) {
throw new Error("Rate limit exceeded. Please wait before trying again.");
}
CORS
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When your frontend (e.g., localhost:5173) requests data from a different origin (e.g., an API server), the API server must explicitly allow it via CORS headers.
If you see a CORS error, the solution is usually:
- Use an API that supports CORS for browser requests
- Proxy requests through your own backend
- Use a serverless function (Vercel Functions) as a proxy
// Do not add headers hoping to fix CORS on the client — CORS is enforced by the server
// These headers must be set by the server, not the browser request:
// Access-Control-Allow-Origin: *
Key Takeaways
- REST APIs use HTTP methods (GET, POST, PATCH, DELETE) to perform CRUD (Create, Read, Update, Delete) actions on resources
- Never hardcode API keys — store them in .env.local and access via import.meta.env.VITE_KEY_NAME
- A custom useFetch hook encapsulates fetch logic and returns { data, loading, error } for clean components
- Always handle all three API states in the UI: loading, error, and success
- Rate limiting (HTTP 429) and CORS errors require different solutions — CORS can only be fixed server-side
- Use a cancelled flag in useEffect to prevent setting state after a component has unmounted
Practice Exercise
- Copy the
useFetchhook into your project'ssrc/hooks/folder - Build a
GitHubProfilecomponent that fetcheshttps://api.github.com/users/YOUR_USERNAMEand displays avatar, name, bio, and follower count - Show a skeleton loading state (grey placeholder boxes) while the data loads
- Handle the case where the username is not found (404 response)
- Add a text input so users can search for any GitHub username
Try it yourself
Key Takeaways
- REST APIs use HTTP methods: GET retrieves, POST creates, PATCH updates, DELETE removes resources
- API keys must be stored in .env.local variables prefixed with VITE_ and never committed to Git
- A custom useFetch hook encapsulates loading, error, and data state so any component can consume it cleanly
- Always display all three API states: loading (skeleton or spinner), error (friendly message), and success (data)
- Rate limiting (HTTP 429) means you are making too many requests; CORS errors are server-side and cannot be fixed client-side
- Use a cancelled flag in useEffect cleanup to prevent setState calls on unmounted components
Quick Quiz
1.What HTTP method should you use to retrieve data from a REST API?
2.Why do we set a cancelled flag in the useFetch useEffect cleanup function?
3.What causes a CORS error and how is it fixed?
4.What is the benefit of extracting fetch logic into a custom useFetch hook?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx