Skip to content

Fetch and axios in React

React doesn’t have built-in HTTP functionality — you use either the browser’s fetch API or a library like axios. This lesson shows both and explains why axios is the better choice for Bulletin.

// Using native fetch
const response = await fetch('http://localhost:3000/posts')
const data = await response.json()

fetch works, but has some rough edges:

  • Always check response.ok — fetch doesn’t throw on 4xx/5xx responses
  • You must call .json() manually
  • No automatic request/response transformation
  • No interceptors (for adding auth headers globally)
// The full fetch pattern with proper error handling
const response = await fetch('http://localhost:3000/posts')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()

axios is a popular HTTP client that solves fetch’s pain points:

Terminal window
npm install axios
import axios from 'axios'
// GET request
const { data } = await axios.get('http://localhost:3000/posts')
// data is already parsed JSON
// POST request
const { data: newPost } = await axios.post('http://localhost:3000/posts', {
title: 'Hello',
body: 'World',
})

Key differences from fetch:

  • Throws on 4xx/5xx — you get proper error handling with try/catch
  • Automatic JSON — parses responses and serializes request bodies
  • Interceptors — add headers to all requests globally (used for auth tokens)
  • TypeScript genericsaxios.get<Post[]>('/posts') types the response
import { useState, useEffect } from 'react'
import axios from 'axios'
import type { Post } from '../types/api'
function PostFeed() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
axios.get<{ posts: Post[] }>('http://localhost:3000/posts')
.then(({ data }) => {
setPosts(data.posts)
setLoading(false)
})
.catch((err) => {
setError(err.response?.data?.error || 'Failed to load posts')
setLoading(false)
})
}, [])
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}

When the API returns a 4xx or 5xx, axios throws. The error object has a response property:

try {
const { data } = await axios.post('/auth/login', credentials)
} catch (err) {
if (axios.isAxiosError(err)) {
const message = err.response?.data?.error || 'Unknown error'
const status = err.response?.status || 0
console.log(`${status}: ${message}`)
}
}

axios.isAxiosError(err) is a type guard that gives you access to the response properties.

  1. Install axios in your bulletin frontend project.
  2. Create a simple component that fetches from GET /posts on mount.
  3. Display the posts in a list, with a loading state and error state.
  4. Test with the API running locally (or your Railway URL).
  • fetch works but requires manual .json() parsing and response.ok checking.
  • axios automatically parses JSON, throws on errors, and supports interceptors.
  • Always handle all three states: loading, success, error.
  • axios.isAxiosError(err) is the type guard for accessing error response data.