← Back

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

  1. What an HTTP request and response are.
  2. How to install and use Axios.
  3. How async and await work.
  4. How to describe API data with TypeScript.
  5. How to save returned data in React state.
  6. How to display loading, error, empty, and success states.
  7. 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:

  1. async marks a function that performs asynchronous work.
  2. axios.get(...) sends a GET request.
  3. await pauses this function until the request finishes.
  4. response is the complete Axios response.
  5. response.data is the data sent by the API.
  6. 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

The form collects one topic and passes it to the parent.

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

Hacker News Search

Console output

Searching for: React

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

Hacker News Search

Console output

API response received

The response contains a hits array.

Submitting the form starts an HTTP request and logs the response data.

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

Hacker News Search

Articles received: 20

After the request, state contains the returned articles and React renders the new count.

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

Search results
Articles with URLs are links; an article without a URL is plain text.

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

  • try contains the request that may fail.
  • catch runs when the request fails.
  • finally runs after success or failure.

We stop loading in finally, so the loading message never remains forever after an error.

Browser Result

While requesting

Loading articles…

If the request fails

Something went wrong. Please try again.

Loading gives feedback; the error message explains when the request failed.

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

Hacker News Search

Search results
The final application collects a topic, loads articles, stores them, and displays the results.

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

  1. Install Axios in a React + TypeScript Vite project.
  2. Create SearchForm with one named topic input.
  3. Send one Hacker News request and log response.data.
  4. Add the Article and HackerNewsResponse interfaces.
  5. Save response.data.hits in state.
  6. Display the articles with stable keys.
  7. Add loading, error, and empty states.
  8. Move the Axios request into articleApi.ts.
  9. 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.

← Back↑ Back to the top