React.js + Next.js · Module 3. Forms and Effects · Lesson 4 · Point 4
HTTP Requests with Axios, TypeScript and React
Do not begin by memorizing an Axios command. First understand the journey of the data.
In this lesson, we follow one search from three points of view:
- the person using the page;
- the React application in the browser;
- the server that answers the request.
We use a small Hacker News search as a safe practice project. Then we look at how the same journey works in a real Pizza Palace application.
You will learn
- What an HTTP request and response are.
- How to install and use Axios.
- How
asyncandawaitwork. - How to describe API data with TypeScript.
- How to save returned data in React state.
- How to display loading, error, empty, and success states.
- How to move request code into a service file.
Examples use normal named function declarations. Props and returned objects are read with explicit property names.
REACT · 01
1. One Request from Three Points of View
An HTTP request is a message sent to a server. An HTTP response is the server's answer.
The same request looks different depending on who is looking at it.
Customer's View
I opened the menu and the pizzas appeared.
React's View
I requested pizza data, waited, saved it in state,
and rendered the menu.
Server's View
I received GET /api/pizzas, read the database,
and returned JSON.
For Pizza Palace, the conversation could look like this:
React application: "Please send me the pizza menu."
↓
HTTP request
↓
Server: "Here is the menu data."
↓
HTTP response
↓
React displays the pizzas
An API is a set of rules and addresses that applications use to communicate.
Examples of requests include:
- loading pizzas from a menu API;
- checking whether ingredients are available;
- searching for articles;
- sending an order to a backend.
This lesson uses the Hacker News Search API because it gives us public article data for practice.
Four Common Request Methods
| Method | Simple Meaning | Pizza Palace Example |
|---|---|---|
GET |
Read data | Load the menu |
POST |
Create or send data | Send a new order |
PATCH |
Change part of existing data | Change the delivery note |
DELETE |
Remove data | Cancel an order |
We will begin with GET because we only need to read articles.
REACT · 02
2. Use Axios as the Messenger
Axios is a JavaScript library that sends HTTP requests.
Install it inside your Vite project:
npm install axios
Then import it:
import axios from "axios";
First Request
import axios from "axios";
async function getArticles() {
const response = await axios.get(
"https://hn.algolia.com/api/v1/search?query=react"
);
console.log(response.data);
}
getArticles();
Read it in order:
asyncmarks a function that performs asynchronous work.axios.get(...)sends aGETrequest.awaitpauses this function until the request finishes.responseis the complete Axios response.response.datais the data sent by the API.getArticles()calls the function and starts the request.
A Simple Meaning for asynchronous
An HTTP request takes time. JavaScript does not freeze the whole page while it waits.
Start request
↓
Wait for the server without freezing the page
↓
Continue when the response arrives
This example logs data but does not create a complete interface, so it does not need a browser preview.
REACT · 03
3. Decide Who Owns Each Job
Our application will contain four main parts:
| Part | Job |
|---|---|
SearchForm |
Collect the search topic |
App |
Manage state and decide what appears |
articleApi |
Communicate with the API |
ArticleList |
Display the returned articles |
Complete Flow
User enters "React"
↓
SearchForm sends the topic to App
↓
App starts loading
↓
articleApi sends an Axios request
↓
Hacker News returns articles
↓
App saves the articles in state
↓
ArticleList displays them
We will build one part at a time.
REACT · 04
4. Let the Form Describe the User's Intention
The form has one job: collect a topic and send it to its parent.
File: src/components/SearchForm.tsx
interface SearchFormProps {
onSearch: (topic: string) => void;
}
export default function SearchForm(props: SearchFormProps) {
function handleSubmit(formData: FormData) {
const topic = formData.get("topic");
if (typeof topic !== "string" || topic.trim() === "") {
alert("Please enter a search topic.");
return;
}
props.onSearch(topic.trim());
}
return (
<form action={handleSubmit}>
<label htmlFor="topic">Search topic</label>
<input
id="topic"
name="topic"
placeholder="For example: React"
/>
<button type="submit">Search</button>
</form>
);
}
Browser Result
Important Lines
The => inside onSearch: (topic: string) => void only describes a TypeScript function type. Our real functions still use normal function declarations.
topic.trim() removes spaces from the beginning and end.
return stops the function when the value is empty.
props.onSearch(topic.trim()) tells the parent what the user wants to search for.
The form does not know anything about Axios. This keeps it reusable.
REACT · 05
5. Let App Receive the Search Intention
The parent creates a named function and passes it to SearchForm.
File: src/App.tsx
import SearchForm from "./components/SearchForm";
export default function App() {
async function handleSearch(topic: string) {
console.log("Searching for:", topic);
}
return (
<main>
<h1>Hacker News Search</h1>
<SearchForm onSearch={handleSearch} />
</main>
);
}
Browser Result
SearchForm collects the topic; App receives it.At this stage, no HTTP request runs. We first confirm that data moves from the child to the parent correctly.
REACT · 06
6. Turn the Topic into a Request Address
Now add Axios inside handleSearch:
import axios from "axios";
import SearchForm from "./components/SearchForm";
export default function App() {
async function handleSearch(topic: string) {
const safeTopic = encodeURIComponent(topic);
const url =
"https://hn.algolia.com/api/v1/search?query=" + safeTopic;
const response = await axios.get(url);
console.log(response.data);
}
return (
<main>
<h1>Hacker News Search</h1>
<SearchForm onSearch={handleSearch} />
</main>
);
}
Browser Result
encodeURIComponent(topic) makes spaces and special characters safe inside a URL.
The request stays in App, not in SearchForm. The form only collects input.
REACT · 07
7. Create a TypeScript Contract for the Response
The Hacker News response contains a hits array. Each item is an article.
We only need three article properties:
interface Article {
objectID: string;
title: string;
url: string | null;
}
url can be text or null because some results do not have an external link.
Now describe the response body:
interface HackerNewsResponse {
hits: Article[];
}
Article[] means an array of Article objects.
Use the type with Axios:
const response = await axios.get<HackerNewsResponse>(url);
TypeScript now understands this expression:
response.data.hits
Read it from left to right:
response = complete Axios response
response.data = response body from the API
.hits = array of articles inside that body
TypeScript checks our code while we develop. It does not prove that a server always sends valid data at runtime.
REACT · 08
8. Use State to Connect Data to the Screen
React must remember the returned articles and render again when they change.
import { useState } from "react";
import axios from "axios";
import SearchForm from "./components/SearchForm";
interface Article {
objectID: string;
title: string;
url: string | null;
}
interface HackerNewsResponse {
hits: Article[];
}
export default function App() {
const [articles, setArticles] = useState<Article[]>([]);
async function handleSearch(topic: string) {
const response = await axios.get<HackerNewsResponse>(
"https://hn.algolia.com/api/v1/search",
{
params: {
query: topic,
},
}
);
setArticles(response.data.hits);
}
return (
<main>
<h1>Hacker News Search</h1>
<SearchForm onSearch={handleSearch} />
<p>Articles received: {articles.length}</p>
</main>
);
}
Browser Result
The state line has three parts:
articles = current article array
setArticles = function that replaces the array
[] = initial empty array
The update flow is:
API returns data
↓
setArticles(response.data.hits)
↓
React saves the new state
↓
React renders App again
REACT · 09
9. Turn Article Data into Visible Links
Move the list into a reusable component.
File: src/types/article.ts
export interface Article {
objectID: string;
title: string;
url: string | null;
}
File: src/components/ArticleList.tsx
import type { Article } from "../types/article";
interface ArticleListProps {
items: Article[];
}
export default function ArticleList(props: ArticleListProps) {
function renderArticle(article: Article) {
if (article.url === null) {
return (
<li key={article.objectID}>
<span>{article.title}</span>
</li>
);
}
return (
<li key={article.objectID}>
<a
href={article.url}
target="_blank"
rel="noopener noreferrer"
>
{article.title}
</a>
</li>
);
}
return <ul>{props.items.map(renderArticle)}</ul>;
}
Browser Result
props.items.map(renderArticle) calls renderArticle once for every item.
key={article.objectID} gives React a stable identifier.
target="_blank" opens an external article in a new tab. rel="noopener noreferrer" protects the original page.
REACT · 10
10. Treat the Request as a Journey with Four Outcomes
An HTTP request does not finish immediately. The user needs to know what is happening.
Our interface has four possible states:
| State | What the User Sees |
|---|---|
| Loading | “Loading articles…” |
| Error | “Something went wrong.” |
| Empty | “No articles found.” |
| Success | The article list |
Create state for the request:
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
Handle the request safely:
async function handleSearch(topic: string) {
setIsLoading(true);
setIsError(false);
setArticles([]);
setHasSearched(true);
try {
const response = await axios.get<HackerNewsResponse>(
"https://hn.algolia.com/api/v1/search",
{
params: {
query: topic,
},
}
);
setArticles(response.data.hits);
} catch {
setIsError(true);
} finally {
setIsLoading(false);
}
}
Read try, catch, and finally
trycontains the request that may fail.catchruns when the request fails.finallyruns after success or failure.
We stop loading in finally, so the loading message never remains forever after an error.
Browser Result
The code above is the request logic, not a complete component, so the preview demonstrates the two visible states it controls.
REACT · 11
11. Hide API Details Behind One Function
As the application grows, keep API code separate from UI code.
Project Structure
src/
├── components/
│ ├── SearchForm.tsx
│ └── ArticleList.tsx
├── services/
│ └── articleApi.ts
├── types/
│ └── article.ts
├── App.tsx
└── main.tsx
File: src/services/articleApi.ts
import axios from "axios";
import type { Article } from "../types/article";
interface HackerNewsResponse {
hits: Article[];
}
export async function searchArticles(topic: string): Promise<Article[]> {
const apiUrl = "https://hn.algolia.com/api/v1/search";
const safeTopic = encodeURIComponent(topic);
const requestUrl = apiUrl + "?query=" + safeTopic;
const response = await axios.get<HackerNewsResponse>(
requestUrl
);
const articles = response.data.hits;
return articles;
}
Read the Function in Five Steps
1. apiUrl = the API address
2. safeTopic = the search text prepared for a URL
3. requestUrl = the complete address for this search
4. response = the answer returned by Axios
5. articles = the article array we return to App
Each variable has one job. This makes the request easier to follow and debug.
What Does the Return Type Mean?
Promise<Article[]>
It means:
This function will finish later and provide an array of articles.
The service knows the API URL and response shape. It does not display JSX and does not manage React state.
App can now use one simple line:
const data = await searchArticles(topic);
REACT · 12
12. Assemble the Complete Request Journey
The complete application uses five files.
File 1: src/types/article.ts
export interface Article {
objectID: string;
title: string;
url: string | null;
}
File 2: src/components/SearchForm.tsx
interface SearchFormProps {
onSearch: (topic: string) => void;
}
export default function SearchForm(props: SearchFormProps) {
function handleSubmit(formData: FormData) {
const topic = formData.get("topic");
if (typeof topic !== "string" || topic.trim() === "") {
alert("Please enter a search topic.");
return;
}
props.onSearch(topic.trim());
}
return (
<form action={handleSubmit}>
<label htmlFor="topic">Search topic</label>
<input id="topic" name="topic" placeholder="Enter a topic" />
<button type="submit">Search</button>
</form>
);
}
File 3: src/components/ArticleList.tsx
import type { Article } from "../types/article";
interface ArticleListProps {
items: Article[];
}
export default function ArticleList(props: ArticleListProps) {
function renderArticle(article: Article) {
if (article.url === null) {
return <li key={article.objectID}>{article.title}</li>;
}
return (
<li key={article.objectID}>
<a
href={article.url}
target="_blank"
rel="noopener noreferrer"
>
{article.title}
</a>
</li>
);
}
return <ul>{props.items.map(renderArticle)}</ul>;
}
File 4: src/services/articleApi.ts
import axios from "axios";
import type { Article } from "../types/article";
interface HackerNewsResponse {
hits: Article[];
}
export async function searchArticles(topic: string): Promise<Article[]> {
const apiUrl = "https://hn.algolia.com/api/v1/search";
const safeTopic = encodeURIComponent(topic);
const requestUrl = apiUrl + "?query=" + safeTopic;
const response = await axios.get<HackerNewsResponse>(
requestUrl
);
const articles = response.data.hits;
return articles;
}
File 5: src/App.tsx
import { useState } from "react";
import SearchForm from "./components/SearchForm";
import ArticleList from "./components/ArticleList";
import { searchArticles } from "./services/articleApi";
import type { Article } from "./types/article";
export default function App() {
const [articles, setArticles] = useState<Article[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
async function handleSearch(topic: string) {
setIsLoading(true);
setIsError(false);
setArticles([]);
setHasSearched(true);
try {
const data = await searchArticles(topic);
setArticles(data);
} catch {
setIsError(true);
} finally {
setIsLoading(false);
}
}
return (
<main>
<h1>Hacker News Search</h1>
<SearchForm onSearch={handleSearch} />
{isLoading && <p role="status">Loading articles…</p>}
{isError && (
<p role="alert">Something went wrong. Please try again.</p>
)}
{!isLoading &&
!isError &&
hasSearched &&
articles.length === 0 && <p>No articles found.</p>}
{articles.length > 0 && <ArticleList items={articles} />}
</main>
);
}
Browser Result
Follow One Successful Search
1. SearchForm sends the topic to App
2. App sets isLoading to true
3. App calls searchArticles(topic)
4. The service sends the Axios request
5. The service returns Article[]
6. App calls setArticles(data)
7. React renders ArticleList
8. finally sets isLoading to false
REACT · 13
13. View the Same Journey inside a Pizza Company
The same architecture can load products for drewno.online or pizzas for Pizza Palace.
The API Can Belong to the Pizza Company
An API does not have to belong to another company.
Pizza Palace can have its own API on its own server:
Customer's browser
React website
↓
Pizza Palace API
↓
Pizza Palace database
React asks the API for information. The API reads the database and returns a response.
React should not connect directly to the database. The backend API protects the database and controls the business rules.
One Server or Several Servers?
A small pizza company may begin with one server:
One company server
├── Pizza Palace backend API
└── Pizza Palace database
The browser still communicates through API addresses such as /api/pizzas and /api/orders.
A larger company may separate the system:
Website and mobile application
↓
API servers
↓
Database
The physical setup is different, but the frontend still communicates with the backend through the API.
Loading Pizza Availability
React can request the current menu:
GET /api/pizzas
The server may return:
[
{
"id": 1,
"name": "Margherita",
"price": 12,
"available": true
},
{
"id": 2,
"name": "Pepperoni",
"price": 14,
"available": false
}
]
React can display Available or Unavailable from this response.
Employee marks Pepperoni unavailable
↓
The database is updated
↓
React requests GET /api/pizzas
↓
The API returns available: false
↓
The website disables the order button
The Server Must Check Again
The information displayed in the browser can become old. Another customer might place an order first, or an employee might change availability.
When the customer submits an order, React sends it to the backend:
POST /api/orders
The server must check the database again before creating the order:
React sends the order
↓
Server checks the latest availability
↓
Available? ── Yes → Create the order
│
No
↓
Return an unavailable message
The browser helps the customer, but the server makes the final decision.
Other Services Are Optional
The Pizza Palace backend may communicate with external services for:
- online payments;
- address or map checks;
- email or SMS messages;
- delivery tracking.
The normal structure is:
React frontend
↓
Pizza Palace API
↓
Pizza Palace database
↓
Optional payment, map, or message services
The menu and availability normally come from the pizza company's own backend and database.
Product Type
interface Product {
id: number;
name: string;
price: number;
stock: number;
}
Product Service
import axios from "axios";
interface Product {
id: number;
name: string;
price: number;
stock: number;
}
export async function fetchProducts(): Promise<Product[]> {
const response = await axios.get<Product[]>("/api/products");
return response.data;
}
The /api/products address is only an example. A real backend must provide it.
Load the Products
async function loadProducts() {
const data = await fetchProducts();
setProducts(data);
}
The same pattern works for pizzas:
User opens menu
↓
App calls fetchPizzas()
↓
Service requests /api/pizzas
↓
App saves pizzas in state
↓
PizzaList displays them
These are architecture snippets, not a complete interface, so they do not need a separate browser preview.
REACT · 14
14. Debug the Journey from the Outside In
When something does not work, follow the data in order. Do not change several files at the same time.
Check 1 — Did the User Action Happen?
Log the value received by handleSearch. If nothing appears, check the form, its name, and props.onSearch.
Check 2 — Was the Request Address Correct?
Log requestUrl and inspect the browser Network panel.
Check 3 — Did the Server Answer?
Check the HTTP status and inspect response.data.
Check 4 — Did State Receive the Data?
Confirm that setArticles(data) receives an array.
Check 5 — Did React Render the Array?
Check the condition that shows ArticleList, then check the list keys and properties.
Frequent Problems
Forgetting await
// Incorrect: response is a Promise, not the finished response.
const response = axios.get("/api/products");
Use await inside an async function.
Saving data in a normal variable
Changing a normal local variable does not tell React to render again. Save visible data in state with setArticles.
Forgetting a list key
Every rendered article needs a stable key, such as article.objectID.
Ignoring request errors
Use try, catch, and finally. Always stop loading after success or failure.
Mixing every responsibility in App
Keep form UI, list UI, request logic, types, and state management in clear places.
Practice
- Install Axios in a React + TypeScript Vite project.
- Create
SearchFormwith one named topic input. - Send one Hacker News request and log
response.data. - Add the
ArticleandHackerNewsResponseinterfaces. - Save
response.data.hitsin state. - Display the articles with stable keys.
- Add loading, error, and empty states.
- Move the Axios request into
articleApi.ts. - Adapt the pattern to load pizzas or products.
Quick Reference
| Code | Meaning |
|---|---|
axios.get(url) |
Send a GET request |
async function |
Create a function that can await asynchronous work |
await |
Wait inside the async function for a Promise |
response.data |
Read the API response body |
setArticles(data) |
Save articles and trigger a new render |
try |
Attempt the request |
catch |
Handle a failed request |
finally |
Run cleanup after success or failure |
| Service file | Keep API communication separate from UI |
Final Pattern
User action
↓
HTTP request
↓
Receive data
↓
Save data in state
↓
Render the interface
Next lesson: Wait for the next source topic before adding more Module 3 material.