API Integration in React
By Syncfusion®, Inc. · Last updated
What an API is, async code, the Fetch API, GET, POST, PUT and DELETE requests, typing and handling API data, loading and error states, a service layer, and environment variables.
What Is an API?
An API (Application Programming Interface) is a way for one program to ask another program for data, or to ask it to do something.
In web apps, an API usually lives on a server. Your React app sends a request to an address, such as https://api.bookcorner.dev/books. The server sends back a response, usually with data in JSON format.
Real-World Example
A bookshop app does not keep its books in the code. When the book list page opens, the app asks the server for the books, shows "Loading…", and then shows the list it receives.
When the admin adds a new book, the app sends it to the server to be saved. If the server is down, the page shows "Could not save the book" and a Try again button.
An API takes a request from the app to the server, and brings back a response
React app asks for data
API GET /books
Server has the data
request request response response
The app never reads the server's data directly. It asks the API, and the API answers.
JSON (JavaScript Object Notation) is a text format for data. It looks almost the same as a JavaScript object:
[
{ "id": 1, "title": "The Alchemist", "price": 299 },
{ "id": 2, "title": "Wings of Fire", "price": 350 }
]
The code boxes on this page cannot reach the internet. Instead, they have a small pretend API at https://api.bookcorner.dev, with three books. Each time you press ▶ Run, it starts again with the same three books. The console shows every request it gets.
Asynchronous Operations
Asking a server for data takes time: maybe half a second, maybe more. If the app stopped and waited, the screen would freeze.
So requests are asynchronous (async). The app starts the request and keeps working. When the answer arrives, your code continues.
Synchronous code waits and blocks. Asynchronous code starts the request, keeps working, and handles the answer later Without async: the app waits and freezes
click
waiting for the server... (screen frozen)
show data With async: the app keeps working
click
show Loading...
user can still click
show data
the request runs in the background
await pauses only your function. The rest of the page keeps working.
JavaScript handles this with a Promise: an object that says "the result will come later". You wait for a Promise with await, inside a function marked async.
async function loadBooks() {
const response = await fetch("https://api.bookcorner.dev/books"); // wait for the answer
const books = await response.json(); // wait for the data
console.log(books);
}
await pauses only this function. The rest of the page, such as buttons and typing, keeps working.
Click Load books, then quickly click the second button many times. The counter still works while the books load. Look at the console: message 2 comes before message 3, because the code did not stop to wait.
?delay=2000 is a setting of the pretend API on this page. It makes the answer take 2 seconds, so you can see the waiting.
Fetch API
fetch is a function built into every browser. It sends a request and gives back a Promise of a response.
Getting data with fetch takes two steps:
await fetch(url)— send the request and wait for the response.await response.json()— read the response body and turn the JSON into JavaScript data.
fetch does not treat a server error, such as 404 "Not Found", as a failure. You must check response.ok yourself. It is true only for status codes from 200 to 299.
The list appears after a short moment. The console shows [API] GET /books → 200: the request the pretend server received.
Look at the effect. useEffect cannot be an async function itself, so we write an async function inside it, and call it.
API Methods
A request has a method. The method tells the server what you want to do.
HTTP methods: GET reads, POST creates, PUT updates, DELETE removes
GET read books
POST add a book
PUT change a book
DELETE remove a book /books /books /books/2 /books/2 Read · Create · Update · Delete
Each method tells the server what you want to do with the data.
These four methods match the four basic data actions: read, create, update, delete.
GET Request
GET reads data. It has no body. You saw it above:
const response = await fetch("https://api.bookcorner.dev/books");
POST Request
POST creates something new. You send the new data in the request body, as JSON.
With fetch, you set the method, a header that says "this is JSON", and the body as text:
const response = await fetch("https://api.bookcorner.dev/books", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Ikigai", author: "Héctor García", price: 399 }),
});
const newBook = await response.json(); // the server adds an id
The server answers with the new book, including the id it gave it.
PUT Request
PUT updates something that already exists. The address names the item, such as /books/2, and the body holds the new values.
await fetch("https://api.bookcorner.dev/books/2", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ price: 320 }),
});
DELETE Request
DELETE removes an item. Like GET, it has no body.
await fetch("https://api.bookcorner.dev/books/2", { method: "DELETE" });
A request has a method, a URL and sometimes a body. A response has a status code and data
Request method: POST url: /books headers: JSON body: { title: "Ikigai" }
Response status: 201 Created data: { id: 4, title: "Ikigai" }
Status codes in the 200s mean success. 400s mean the request was wrong. 500s mean the server failed.
The lab below uses all four methods.
Add a book, raise a price and remove a book. Watch the console: each button sends a different method, followed by a GET to load the new list.
sendJson is a small helper, so the POST and PUT code is not written twice. It sets the method, the JSON header and the body.
Working with API Data
Interfaces for API Data
The data from an API has a fixed shape. Describe that shape with an interface, so TypeScript knows which fields exist.
interface Book {
id: number;
title: string;
author: string;
price: number;
}
Now book.titel (a spelling mistake) is underlined at once, instead of showing undefined on the page later.
Typed API Response Models
A response model is an interface for the whole answer, not just one item. Many APIs wrap the data with extra information, such as the total count. You can also describe the error shape.
// What GET /books gives back: an array of books
type BookListResponse = Book[];
// What the API gives back when something goes wrong
interface ApiError {
message: string;
}
With fetch, you give the type to the variable that holds the data:
const data: Book[] = await response.json();
Now TypeScript treats data as a list of books.
TypeScript only trusts these types. It does not check the real data while the app runs. If the server sends something different, the types will be wrong. That is why you still handle errors.
Handling API Responses
A response can be a success or an error. Check the status code:
| Status | Meaning | What to do |
|---|---|---|
| 200 OK | It worked | Use the data |
| 201 Created | A new item was made | Use the new item, or reload the list |
| 204 No Content | It worked, no data sent back | Nothing to read |
| 400 Bad Request | Your data was wrong | Show the server's message |
| 404 Not Found | The item does not exist | Show "not found" |
| 500 Server Error | The server failed | Show "try again later" |
Try each button. Book 1 is found. Book 99 gives a 404 with the server's message. The broken server gives a 500.
The catch part runs only when the request cannot be sent at all, for example when there is no internet.
Displaying API Data
Once the data is in state, you show it like any other state, usually with map(). Plan for the empty case too, so the screen never looks broken.
{books.length === 0 ? (
<p>No books found.</p>
) : (
<ul>{books.map((b) => <li key={b.id}>{b.title}</li>)}</ul>
)}
Use the item's id from the API as the key. It is unique and does not change.
API Loading States
Every API call goes through a few states. The screen should show something useful in each one.
Loading states: idle, then loading, then either success with data or an error
idle nothing yet
fetch
loading show spinner
ok
success show the data
fails
error show a message
Every API call goes through these states. Plan what the screen shows in each one.
- Loading — show "Loading..." or a spinner, and disable buttons that would send the request again.
- Success — show the data.
- Error — show a clear message, and a way to try again.
The list shows "Loading books..." first. Tick Break the server to see the error and the Try again button.
finally runs at the end whether the request worked or failed. So setIsLoading(false) is written once, and "Loading..." never gets stuck.
API Service Layer Pattern
If every component writes its own fetch calls, the same URLs and error code are copied everywhere. A service layer fixes this: you put all API calls in a few small files, and components just call simple functions like getBooks().
Service layer: components call bookService, which uses one API client, which talks to the server
Components show the screen
bookService getBooks()
apiClient fetch helper
API
Each layer has one job. Components never build URLs themselves.
- apiClient.ts — one small helper around
fetch. It adds the base address, sends JSON, checksresponse.ok, and reads the JSON. - bookService.ts — one function for each request:
getBooks,addBook,deleteBook. Each one returns typed data. - Components — call the service functions and show the result. They never build URLs.
Look at App.tsx. It has no URLs and no fetch code. It just calls getBooks(), addBook() and deleteBook(). If the API address changes, you change only apiClient.ts.
apiRequest<T> uses a type parameter T. Each service function says what type it expects back, such as apiRequest<Book[]>, so the result is typed.
Omit<Book, "id"> means "a Book, but without the id field". A new book has no id yet, because the server gives it one.
Environment Variables
An environment variable is a setting that lives outside your code, such as the API address. You may use a test server while you build the app, and the real server when it goes live. With environment variables, you change the address without changing the code.
In a Vite project, you write them in a file called .env, in the project folder:
VITE_API_URL=https://api.bookcorner.dev
Then you read them in your code with import.meta.env:
const response = await fetch(import.meta.env.VITE_API_URL + "/books");
Environment variables: the .env file is read by Vite, and your code reads the value with import.meta.env
.env VITE_API_URL=https://...
Vite reads it at start
Your code import.meta.env.VITE_API_URL
Change the .env file, not the code, when the address changes.
Vite only gives your app the variables whose names start with VITE_. Others stay hidden. This stops you from sending a secret to the browser by mistake.
Change VITE_SHOP_NAME in the .env tab and press ▶ Run. The heading changes, and you did not touch App.tsx.
In a real Vite project, restart npm run dev after you change .env, because Vite reads the file only when it starts.
Never put secrets, such as passwords or private API keys, in a VITE_ variable. Everything in your React code can be read by anyone who opens the website. Secrets belong on the server.
Capstone Project: User Management System
You will build a User Management System for the Book Corner team: a page to see, add, edit and delete users. It talks to the pretend API at https://api.bookcorner.dev/users using only the Fetch API, and puts every part of this lesson together: a service layer, typed data, loading states and friendly errors.
The files and their jobs:
types.ts: theUsertype andNewUser(a user without an id).apiClient.ts: onerequest()helper aroundfetch.userService.ts: the API service layer:getAll,create,update,remove.validateUser.ts: form validation rules.useUsers.ts: a custom hook that holds the users, the loading and error state, and the CRUD actions.UserTable.tsxandUserForm.tsx: the screen.
API Service Layer
Components never call fetch directly. They call userService, which calls request() in apiClient.ts. So the base URL, the JSON headers and the error rules are written once.
export const userService = {
getAll: () => request<User[]>("/users"),
create: (user: NewUser) => request<User>("/users", { method: "POST", body: user }),
update: (user: User) => request<User>("/users/" + user.id, { method: "PUT", body: user }),
remove: (id: number) => request<void>("/users/" + id, { method: "DELETE" }),
};
Fetch Users
useUsers calls userService.getAll() in a useEffect when the page opens, and saves the result with setUsers.
Display User List
UserTable maps the users to table rows with key={u.id}. Each row shows the name, email and role, plus Edit and Delete buttons. An empty list shows "No users yet".
Loading States
While the first request runs, loading is true, so the page shows Loading users... instead of an empty table. During add, edit and delete, saving is true: the save button says Saving... and is disabled, so it cannot be clicked twice.
Run the lab: it fetches the users through the service layer, shows the loading text, then the list.
Add New User
UserForm is a controlled form with a name, an email and a role. On submit, it calls add(user). The hook sends a POST through userService.create, and adds the user the server returns (now with an id) to the list.
Edit User
Clicking Edit puts that user in editing state. The form fills itself with the user's details and the button changes to Save changes. Saving sends a PUT with userService.update, and the row is replaced with the server's answer. Cancel leaves edit mode.
Delete User
Clicking Delete first asks "Delete? Yes / No" in the same row, so nobody deletes a user by mistake. Yes sends a DELETE with userService.remove, and removes the row.
Form Validation
Before any request, validateUser checks the form:
- The name is required and needs at least 3 characters.
- The email is required, must look like
name@example.com, and must not belong to another user.
Each message shows under its field, and nothing is sent until the form is valid. This saves a slow trip to the server, but the server should still check the data too.
CRUD Operations
Together these are the four CRUD operations:
| Operation | HTTP method | Service function | Screen action |
|---|---|---|---|
| Create | POST /users | create(user) | Add user |
| Read | GET /users | getAll() | The list on page load |
| Update | PUT /users/:id | update(user) | Save changes |
| Delete | DELETE /users/:id | remove(id) | Delete → Yes |
After each successful request, the hook updates the users state with the server's answer, so the screen always matches the data.
Error Handling
request() turns every failure into an ApiError with a friendly message: no internet, a bad request (400), not found (404) or a server problem (500 or more). The hook catches it, keeps the current list, and shows the message in a red box with a Dismiss button.
Here is the complete system. Add, edit and delete users. Try invalid input. Then tick Simulate a server error and try again to see the error handling. Untick it and press Reload.
Each request updates the list from the server's answer (the created or updated user), not from the form. Then the screen shows exactly what was saved, including the new id.
Download Project Files
User Management Project Source Code
The complete User Management System is ready as a real Vite + React + TypeScript project. A small REST API made with json-server runs next to it and saves the users in db.json. The code uses the Fetch API only.
Download User Management Project: Download User Management Project
user-management/
package.json scripts: the React app and the API together
db.json the users (json-server saves changes here)
.env VITE_API_URL=http://localhost:3001
src/
main.tsx, App.tsx
types/user.ts User, NewUser, Role
api/httpClient.ts request() helper and ApiError
services/userService.ts getAll, getById, create, update, remove
hooks/useUsers.ts users, loading, saving, error + CRUD actions
utils/validateUser.ts form validation rules
components/ UserTable, UserForm, SearchBar, Alert, ConfirmDelete
index.css
Project Setup Instructions
- Install Node.js 18 or newer from nodejs.org, if you do not have it.
- Download the ZIP with the link above, and unzip it. You get a folder called
user-management. - Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
- Install the packages and start the app and the API together:
npm install
npm run dev
The app opens at http://localhost:5173, and the API runs at http://localhost:3001/users. Open db.json while you add, edit and delete users, and watch it change. Stop the API (or change VITE_API_URL in .env) to see the error handling. Press Ctrl + C to stop.
Common mistakes
| Mistake | What you see | Fix |
|---|---|---|
Forgetting await before fetch or response.json() | You get a Promise, not the data | Add await, inside an async function |
Making the useEffect function itself async | A warning, and cleanup does not work | Write an async function inside the effect and call it |
Not checking response.ok with fetch | A 404 error page is treated as data | Check response.ok before using the data |
Sending an object as the fetch body | The server gets [object Object] | Use JSON.stringify(...) and the JSON header |
Not turning isLoading off after an error | "Loading..." never goes away | Use finally { setIsLoading(false) } |
| Copying API URLs into every component | Hard to change the address later | Use a service layer and one API client |
An env variable without the VITE_ start | import.meta.env.API_URL is undefined | Name it VITE_API_URL |
A secret key in .env with VITE_ | Anyone can read it in the browser | Keep secrets on the server |
Check Yourself
Exercises
What to Remember
- An API is how your app asks a server for data. Requests are async, so use
asyncandawait. fetchis built into the browser. Checkresponse.ok, and callresponse.json()to read the data.- GET reads, POST creates, PUT updates, DELETE removes. Type the data with interfaces, show loading and error states, keep API calls in a service layer, and read settings from
.env.
Next: Styling Components