From Database to Desktop: A Journey Through HTTP, CORS, and Modern Data Fetching in React

Every time a React app shows you a list of products, something remarkable has already happened behind the scenes.
A row of data sitting in a database on some server, possibly thousands of miles away, has traveled across the internet, passed a security checkpoint, and landed in your browser as usable JavaScript.
This post walks through that entire journey.
Table of Contents
- How Does Data Get From a Database to Your Desktop?
- Axios
- TanStack Query
- SWR
- RTK Query
- Bringing It All Together
- Glossary
1. How Does Data Get From a Database to Your Desktop?
Before talking about libraries like Axios or TanStack Query, it's worth understanding what's actually happening under the hood.
Three concepts form the foundation of every data-fetching library you'll ever use:
the HTTP Request, CORS, and the Fetch API.
The HTTP Request
Nothing moves without a request. When your React app needs data, it doesn't reach into the database directly (that would be a massive security risk).
Instead, it sends an HTTP request to a server, which acts as a gatekeeper.
The server
- queries the database,
- formats the result (usually as JSON), and
- sends it back as an HTTP response.
A typical request has four key parts:
- Method —
GETto read data,POSTto create it,PUT/PATCHto update it,DELETEto remove it - URL — the endpoint, e.g.
https://api.example.com/users - Headers — metadata like
Content-Typeor anAuthorizationtoken - Body — the payload, used for
POST/PUTrequests
I was building a logistics and delivery management system with my team, and this is how we planned the request and response before we actually started the project.

If you want to know more about this project, please check out the link below.
>> Blog Post Link
So the real flow looks like this
Browser → HTTP Request → Server → Query → Database
Browser ← HTTP Response ← Server ← Result ← DatabaseThe database never talks to your browser directly. The server is always the middleman.
CORS: The Security Checkpoint
Here's where many developers hit their first wall. Browsers enforce a rule called the Same-Origin Policy.
A default browser rule that says a web page loaded from one origin (domain + protocol + port) cannot read responses from a different origin — unless that other origin explicitly allows it.
In practice: https://myapp.com is not allowed to fetch data from https://api.otherdomain.com unless the second server says it's okay.
CORS (Cross-Origin Resource Sharing) is the mechanism that safely relaxes this restriction.
A set of HTTP headers that let a server tell the browser "this other origin is allowed to read my response." Without the right header, the browser blocks the response even if the server successfully returned it.
The server does this simply by including a response header:
Access-Control-Allow-Origin: https://myapp.com
If that header is missing or doesn't match, the browser blocks the response.
Example 1 — Single Allowed Origin
Yogo-App ("Your Logo") is a project that detects a logo by uploading an image.

origin: only requests coming from this exact frontend URL get theAccess-Control-Allow-Originheader back — every other origin gets blocked by the browser.methods: onlyGET,POST, andOPTIONSare allowed.OPTIONSshows up because of the preflight request.
Before sending certain "risky" requests (like aPOSTwith aContent-Type: application/jsonheader), the browser first fires a silentOPTIONSrequest asking "is this actually allowed?" Only if the server says yes does the browser send the real request.
Code: Yogo-App/server/server.js
Real Example 2 — Multiple Allowed Origins
signUpPage shows the more flexible pattern: allowing several origins at once, and separating local development from production using an environment variable.

- Instead of a single string,
originis now a function that runs on every incoming request. allowedOriginsis an array mixing hardcoded local-dev URLs withprocess.env.FRONTEND_URL, so the same code works in development and production without editing the source..filter(Boolean)quietly removesprocess.env.FRONTEND_URLfrom the array if it'sundefined(e.g., not set locally) — a common defensive-coding trick.if (!origin)allows requests with no origin header at all, like Postman or server-to-server calls, which don't send anOriginheader.
Code: signUpPage/backend/server.js
The Fetch API: The Browser's Native Tool
Once a request is allowed to happen, something has to actually send it.
The Fetch API is the browser's built-in, promise based way of doing this
Characteristics:
- Built directly into the browser - zero dependencies
- Promise-based
- Does not automatically parse JSON - you must call
.json()yourself - Does not reject the promise on HTTP error statuses like 404 or 500 -you must check
response.okmanually - No built-in timeout, retry, or interceptor system
Real Example — Weather-App

This is Fetch's rough edges on full display:
if (!response.ok) throw new Error(...)— this line only exists because Fetch won't throw on its own for a 404/500 responseawait response.json()— the manual parsing step Axios would have done automatically
Code: Weather-App/script.js
2. Axios
Axios is a third-party HTTP client that wraps the same underlying browser networking but smooths out Fetch's rough edges.
import axios from 'axios';
const { data } = await axios.get('https://api.example.com/users');
console.log(data); // already parsed JSON
Characteristics:
- Automatic JSON parsing — no manual
.json()call needed - Automatic error rejection — a 404 or 500 response throws, so
try/catchworks as expected - Interceptors — hooks into every outgoing request or incoming response, perfect for attaching auth tokens or handling token refresh globally
- Built-in timeout and cancellation support
- Consistent behavior across browsers and Node.js, which matters for server-side rendering
- Supports creating reusable, pre-configured instances via
axios.create()
Real Example 1 — A Custom Axios Instance
Instead of importing raw axios everywhere, church-login-website creates one configured instance and reuses it across the whole app:

baseURL
A prefix automatically added to every request made with this instance, so you write axios.get("/api/user") instead of the full URL every time.withCredentials
Tells the browser to include cookies (like session cookies) in cross-origin requests. Without it, the browser strips cookies from any request going to a different origin.
withXSRFTokenCSRF(Cross-Site Request Forgery)
A type of attack where a malicious site tricks your browser into sending a request to a site you're logged into, using your cookies without your consent. A CSRF token is a random value the server issues and the client must echo back, proving the request really came from your own app.
Code: church-login-website/frontend/src/lib/axios.js
Real Example 2 — Axios + SWR Working Together
This hook shows Axios and SWR cooperating: Axios sends the requests, SWR manages the resulting state.
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import useSWR from "swr";
import axios from "../lib/axios";
export const useAuth = ({ middleware } = {}) => {
const router = useRouter();
const fetcher = async () => {
try {
const res = await axios.get("/api/user", {
withCredentials: true,
});
return res.data.data;
} catch (error) {
if (error.response?.status === 401) return null;
throw error;
}
};
const { data: user, error, mutate } = useSWR(
"/api/user",
fetcher,
{
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
}
);
const csrf = () =>
axios.get("/sanctum/csrf-cookie", { withCredentials: true });
// Login
const login = async ({ setErrors, ...props }) => {
setErrors([]);
await csrf();
try {
await axios.post("/api/login", props, {
withCredentials: true,
});
await mutate();
router.replace("/user/dashboard");
} catch (error) {
if (error.response?.status !== 422) throw error;
setErrors(Object.values(error.response.data.errors).flat());
}
};
// Logout
const logout = async () => {
try {
await csrf();
// Web-session logout endpoint for Planning Center flow.
await axios.post("/logout", {}, { withCredentials: true });
// Immediately clear cached user and avoid stale UI state.
await mutate(null, false);
router.replace("/");
} catch (error) {
console.error("Logout error:", error);
}
};
useEffect(() => {
if (user === undefined) return;
if (middleware === "guest" && user) {
router.push("/");
}
if (middleware === "auth" && user === null) {
router.push("/login");
}
}, [user, middleware, router]);
return {
user,
csrf,
isLoading: user === undefined,
login,
logout,
};
};fetcherwraps a plainaxios.get()call — this is the "how do I send the request" layeruseSWR("/api/user", fetcher, {...})hands that fetcher to SWR, which now owns caching and revalidation — this is the "how do I manage the resulting state" layermutate()
A function SWR gives you to manually tell it "the cached data is now stale/wrong, please update it" — either by refetching, or by passing a new value directly (as seen inlogout:mutate(null, false)instantly clears the cached user without waiting for a network round trip).
csrf()calls Laravel Sanctum's/sanctum/csrf-cookieendpoint beforelogin/logout— this is the CSRF handshake mentioned above, required before any cookie-authenticatedPOST.
Code: church-login-website/frontend/src/hooks/auth.js
Axios solved the sending problem elegantly.
But it still leaves a bigger question unanswered: once you have the data, how do you keep it in sync with the server, cache it, avoid re-fetching it unnecessarily, and handle loading and error states consistently across your app? That's a different layer of the problem entirely, and it's where TanStack Query and SWR enter the story.
3. TanStack Query
TanStack Query (formerly React Query) is not a replacement for Axios or Fetch.
It doesn't send requests itself. Instead, it manages what happens around a request.
- Caching, background refetching, retries, and synchronizing "server state" with your UI.
Data that actually lives on a server/database and is only ever a temporary, possibly-stale copy in your browser — as opposed to "client state" like whether a modal is open, which lives entirely in the browser.
Characteristics:
- Caching by key — calling the same query in five different components triggers one network request, not five
- Automatic background refetching — data refreshes when the window regains focus or the network reconnects
- Deduplication — simultaneous identical requests are merged into one
- Built-in loading, error, and success states — no more manually managing
useStatefor each of these - Framework-agnostic — official adapters exist for React, Vue, Svelte, Solid, and Angular
- Supports advanced features out of the box: mutations, infinite/paginated queries, offline support
Real Example 1 — Global Query Client Configuration

staleTime
How long fetched data is considered "fresh" before TanStack Query is willing to refetch it in the background. Here, 60 * 1000 ms (1 minute) — during that window, repeated calls to the same query just reuse the cache, no network request happens.retry: 1— if a query fails, try one more time automatically before showing an errorrefetchOnWindowFocus: false— disables the default behavior of silently refetching every time the browser tab regains focus
Code: aim_frontend/aim-client/lib/queryClient.js
Real Example 2 — A Query Hook Built on Axios

fetchBulletinsis plain Axios — again, TanStack Query doesn't care how the data is fetched, only thatqueryFnreturns a PromisequeryKey
A unique array-based identifier for this specific query's cached data.["bulletins", page]means page 1 and page 2 of bulletins are cached completely separately — changingpageautomatically triggers a new (or cached) fetch.
gcTime(garbage collection time)
How long unused cached data is kept in memory before being deleted entirely. Even after data goes stale (paststaleTime), it stays available for instant re-display untilgcTimeexpires.
Code: aim_frontend/aim-client/features/home/hooks/api/useBulletins.js
In short
Axios answers "how do I send this request?" TanStack Query answers "how do I keep this data fresh, cached, and in sync across my whole app?" They work at different layers, and that's exactly why they're usually used together.
4. SWR
Now that we've seen Axios and SWR working together in the useAuth hook above, it's worth zooming in on SWR itself.
SWR, built by Vercel, solves the same core problem as TanStack Query.
- server-state caching and synchronization
but with a smaller footprint and a simpler mental model. Its name comes from the HTTP caching strategy "stale-while-revalidate".
Show the cached (possibly outdated/"stale") data to the user immediately, while quietly fetching fresh data in the background — the UI never has to show a blank loading state if a cached copy already exists.
Characteristics:
- You still supply your own fetcher function (Axios, Fetch, or anything Promise-based) — SWR doesn't send requests itself, same as TanStack Query
- Smaller API surface and leaner bundle size than TanStack Query
- Focused tightly on data fetching, with less built-in scope creep
- Revalidation behavior (
revalidateOnFocus,revalidateOnReconnect, etc.) is highly configurable per-hook mutate()gives direct, manual control over the cache — useful for optimistic UI updates like instantly clearing user state on logout
Real Example — Revisiting useAuth
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import useSWR from "swr";
import axios from "../lib/axios";
export const useAuth = ({ middleware } = {}) => {
const router = useRouter();
const fetcher = async () => {
try {
const res = await axios.get("/api/user", {
withCredentials: true,
});
return res.data.data;
} catch (error) {
if (error.response?.status === 401) return null;
throw error;
}
};
const { data: user, error, mutate } = useSWR(
"/api/user",
fetcher,
{
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
}
);
const csrf = () =>
axios.get("/sanctum/csrf-cookie", { withCredentials: true });
// Login
const login = async ({ setErrors, ...props }) => {
setErrors([]);
await csrf();
try {
await axios.post("/api/login", props, {
withCredentials: true,
});
await mutate();
router.replace("/user/dashboard");
} catch (error) {
if (error.response?.status !== 422) throw error;
setErrors(Object.values(error.response.data.errors).flat());
}
};
// Logout
const logout = async () => {
try {
await csrf();
// Web-session logout endpoint for Planning Center flow.
await axios.post("/logout", {}, { withCredentials: true });
// Immediately clear cached user and avoid stale UI state.
await mutate(null, false);
router.replace("/");
} catch (error) {
console.error("Logout error:", error);
}
};
useEffect(() => {
if (user === undefined) return;
if (middleware === "guest" && user) {
router.push("/");
}
// ✅ FIX: redirect to real page, not API
if (middleware === "auth" && user === null) {
router.push("/login");
}
}, [user, middleware, router]);
return {
user,
csrf,
isLoading: user === undefined,
login,
logout,
};
};- The first argument,
"/api/user", is the cache key — similar in spirit to TanStack Query'squeryKey - All three
revalidateOn...options are turned off here, meaning: fetch once, then trust the cache completely untilmutate()is called manually. This makes sense for auth state — you don't want to silently re-check "who is logged in" every time the tab regains focus.
Code: church-login-website/frontend/src/hooks/auth.js
If TanStack Query is the fully-loaded toolkit, SWR is the minimalist's version of the same idea — same destination, lighter luggage.
5. RTK Query
The last stop on this journey is RTK Query, part of Redux Toolkit. If your app already manages global state with Redux, RTK Query extends that same store to handle server state too — instead of bolting on a separate caching library, your API data lives right alongside your Redux state.
Characteristics:
- Built directly into Redux Toolkit
The officially recommended, opinionated way to write Redux logic, designed to reduce boilerplate compared to "vanilla" Redux.
- Auto-generates React hooks (like
useGetUsersQuery) from a single API definition - Cache invalidation is handled through tags — you label which data a mutation affects, and RTK Query automatically refetches anything tagged with it
- Integrates directly with Redux DevTools, so cached API data is visible and debuggable the same way as regular Redux state
- Best suited for apps already committed to Redux — otherwise it adds Redux as a dependency just to get data-fetching features
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
endpoints: (builder) => ({
getUsers: builder.query({ query: () => '/users' }),
}),
});
export const { useGetUsersQuery } = api;
function UserList() {
const { data, isLoading, error } = useGetUsersQuery();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Something went wrong</p>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}Notice the pattern is the same shape as SWR and TanStack Query — a hook, a loading state, an error state, and data.
RTK Query gives you
- caching, automatic re-fetching, and cache invalidation via tags, all wired directly into the Redux store's dev tools.
Its main advantage isn't a fundamentally different approach.
It's integration: if you're already committed to Redux for state management, RTK Query means one mental model and one store, instead of Redux for client state plus a separate library for server state.
Bringing It All Together
- HTTP is the language your browser and server use to talk to each other.
- CORS is the browser's security guard, deciding whether a cross-origin response is allowed to reach your JavaScript.
- Fetch is the browser's native way of actually sending that HTTP request.
- Axios improves on Fetch's ergonomics — easier JSON handling, real error rejection, interceptors, reusable instances.
- TanStack Query, SWR, and RTK Query all sit one layer above Axios/Fetch. They don't replace the request mechanism — they manage what happens to the data after it arrives: caching it, keeping it fresh, deduplicating requests, and syncing it with your UI.
Understanding this layering is the key insight
Once you see the two layers clearly, choosing between Axios vs. Fetch, or TanStack Query vs. SWR vs. RTK Query.

Glossary
HTTP Request
A message the browser sends to a server asking for or submitting data. Made of a method, URL, headers, and (optionally) a body.
HTTP Response
The server's reply to an HTTP request, usually containing a status code and a JSON body.
JWT (JSON Web Token) / Access Token
A signed, self-contained token issued after login that proves a user's identity without the server needing to check a session store on every request.
Same-Origin Policy
The browser's default rule that a page can't read responses from a different origin unless that origin explicitly allows it.
CORS (Cross-Origin Resource Sharing)
The set of HTTP headers that relax the Same-Origin Policy, letting a server explicitly allow specific outside origins to read its responses.
Origin
The combination of protocol + domain + port that defines where a request "comes from" (e.g., https://myapp.com).
Preflight Request
An automatic OPTIONS request the browser sends before certain "risky" cross-origin requests, asking the server for permission before sending the real one.
Promise
A JavaScript object representing a value that isn't available yet but will resolve (succeed) or reject (fail) in the future. The basis of async/await.
baseURL
A prefix automatically prepended to every request made through a configured Axios instance.
withCredentials
An option that tells the browser to include cookies in cross-origin requests, which are stripped by default.
CSRF (Cross-Site Request Forgery)
An attack where a malicious site tricks a logged-in user's browser into making an unwanted request using their existing cookies.
CSRF Token
A random, server-issued value the client must send back to prove a request genuinely came from the app itself, defending against CSRF attacks.
Interceptor
A function that runs automatically on every outgoing request or incoming response in Axios — commonly used to attach auth tokens or handle errors globally.
Server State
Data that actually lives on a server/database and is only ever a temporary copy in the browser — distinct from "client state" like UI toggles.
queryKey
A unique identifier (usually an array) TanStack Query uses to cache and look up a specific query's data.
queryFn
The function you provide to TanStack Query that actually performs the fetch (using Axios, Fetch, etc.) and returns a Promise.
staleTime
How long fetched data is considered "fresh" before the library is willing to refetch it automatically.
gcTime (garbage collection time)
How long unused cached data stays in memory before being deleted entirely, even after it's gone stale.
mutate()
A function (in SWR or TanStack Query's useMutation) used to manually trigger a refetch or directly overwrite cached data — useful for instant UI updates.
Deduplication
Automatically merging multiple identical, simultaneous requests into a single network call.
Stale-While-Revalidate
A caching strategy (and SWR's namesake): show cached data immediately, then silently fetch a fresh copy in the background.
Redux Toolkit
The officially recommended, opinionated toolkit for writing Redux logic with less boilerplate than "vanilla" Redux.
Tag-based Invalidation
RTK Query's cache-invalidation system, where you label ("tag") which data a mutation affects so related queries automatically refetch.
