# API Integration

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Working with APIs
Source: https://www.devtutorials.ai/learn/react/apis/api-integration

> 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.

<figure className="react-fig">
<svg viewBox="0 0 520 130" role="img" aria-labelledby="fig-p-api" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-api">An API takes a request from the app to the server, and brings back a response</title>
  <rect x="10" y="40" width="130" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="75.0" y="62.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>React app</text>
  <text x="75.0" y="79.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>asks for data</text>
  <rect x="195" y="40" width="130" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="62.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>API</text>
  <text x="260.0" y="79.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>GET /books</text>
  <rect x="380" y="40" width="130" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="445.0" y="62.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Server</text>
  <text x="445.0" y="79.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>has the data</text>
  <path d="M140 55 L191 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M191 55 L182.7 58.5 L182.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M325 55 L376 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M376 55 L367.7 58.5 L367.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M376 78 L329 78" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M329 78 L337.3 74.5 L337.3 81.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M191 78 L144 78" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M144 78 L152.3 74.5 L152.3 81.5 Z" style={{ fill: "var(--faint)" }} />
  <text x="165" y="46" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 10, fontWeight: 400, fontFamily: "var(--sans)" }}>request</text>
  <text x="350" y="46" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 10, fontWeight: 400, fontFamily: "var(--sans)" }}>request</text>
  <text x="350" y="98" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 10, fontWeight: 400, fontFamily: "var(--sans)" }}>response</text>
  <text x="165" y="98" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 10, fontWeight: 400, fontFamily: "var(--sans)" }}>response</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="3.2s" repeatCount="indefinite" path="M140 55 L191 55 L325 55 L376 55 M376 78 L329 78 L191 78 L144 78" />
  </circle>
</svg>
<figcaption>The app never reads the server's data directly. It asks the API, and the API answers.</figcaption>
</figure>

**JSON** (JavaScript Object Notation) is a text format for data. It looks almost the same as a JavaScript object:

```json
[
  { "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.

<figure className="react-fig">
<svg viewBox="0 0 520 196" role="img" aria-labelledby="fig-p-async" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-async">Synchronous code waits and blocks. Asynchronous code starts the request, keeps working, and handles the answer later</title>
  <text x="10" y="22" textAnchor="start" style={{ fill: "var(--bad)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Without async: the app waits and freezes</text>
  <rect x="10" y="32" width="90" height="34" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="55.0" y="54.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>click</text>
  <rect x="105" y="32" width="300" height="34" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="255.0" y="54.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>waiting for the server... (screen frozen)</text>
  <rect x="410" y="32" width="100" height="34" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="460.0" y="54.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>show data</text>
  <text x="10" y="98" textAnchor="start" style={{ fill: "var(--ok)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>With async: the app keeps working</text>
  <rect x="10" y="108" width="90" height="34" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="55.0" y="130.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>click</text>
  <rect x="105" y="108" width="145" height="34" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="177.5" y="130.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>show Loading...</text>
  <rect x="255" y="108" width="150" height="34" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="330.0" y="130.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>user can still click</text>
  <rect x="410" y="108" width="100" height="34" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="460.0" y="130.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>show data</text>
  <path d="M100 146 Q255 170 410 146" style={{ fill: "none", stroke: "var(--accent-secondary)", strokeWidth: 2, strokeDasharray: "5 4" }} />
  <text x="255" y="188" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>the request runs in the background</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2.6s" repeatCount="indefinite" path="M100 146 Q255 170 410 146" />
  </circle>
</svg>
<figcaption>await pauses only your function. The rest of the page keeps working.</figcaption>
</figure>

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`.

```tsx
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.

      <button onClick={() => { loadBooks(); console.log("2. Code after the request runs at once"); }}>
        Load books (takes 2 seconds)
      </button>{" "}
      <button onClick={() => setClicks(clicks + 1)}>I still work: {clicks}</button>
      <p>{message}</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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:

1. `await fetch(url)` — send the request and wait for the response.
2. `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.

      {books.map((book) => (
        <li key={book.id}>{book.title} · ₹{book.price}</li>
      ))}
    </ul>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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.

<figure className="react-fig">
<svg viewBox="0 0 520 130" role="img" aria-labelledby="fig-p-methods" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-methods">HTTP methods: GET reads, POST creates, PUT updates, DELETE removes</title>
  <rect x="5" y="20" width="120" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="65.0" y="42.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>GET</text>
  <text x="65.0" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>read books</text>
  <rect x="135" y="20" width="120" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="195.0" y="42.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>POST</text>
  <text x="195.0" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>add a book</text>
  <rect x="265" y="20" width="120" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="325.0" y="42.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>PUT</text>
  <text x="325.0" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>change a book</text>
  <rect x="395" y="20" width="120" height="50" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="455.0" y="42.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>DELETE</text>
  <text x="455.0" y="59.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>remove a book</text>
  <text x="65" y="95" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>/books</text>
  <text x="195" y="95" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>/books</text>
  <text x="325" y="95" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>/books/2</text>
  <text x="455" y="95" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>/books/2</text>
  <text x="260" y="120" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 600, fontFamily: "var(--sans)" }}>Read · Create · Update · Delete</text>
</svg>
<figcaption>Each method tells the server what you want to do with the data.</figcaption>
</figure>

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:

```tsx
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:

```tsx
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.

```tsx
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.

```tsx
await fetch("https://api.bookcorner.dev/books/2", { method: "DELETE" });
```

<figure className="react-fig">
<svg viewBox="0 0 520 170" role="img" aria-labelledby="fig-p-reqres" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-reqres">A request has a method, a URL and sometimes a body. A response has a status code and data</title>
  <rect x="10" y="10" width="230" height="150" rx="10" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)" }} />
  <text x="125" y="32" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 14, fontWeight: 700, fontFamily: "var(--sans)" }}>Request</text>
  <text x="25" y="60" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>method:  POST</text>
  <text x="25" y="82" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>url:     /books</text>
  <text x="25" y="104" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>headers: JSON</text>
  <text x="25" y="126" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>body:    &#123; title: "Ikigai" &#125;</text>
  <rect x="280" y="10" width="230" height="150" rx="10" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)" }} />
  <text x="395" y="32" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 14, fontWeight: 700, fontFamily: "var(--sans)" }}>Response</text>
  <text x="295" y="60" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>status: 201 Created</text>
  <text x="295" y="82" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>data:   &#123; id: 4,</text>
  <text x="295" y="104" textAnchor="start" style={{ fill: "var(--ink2)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>          title: "Ikigai" &#125;</text>
  <path d="M240 60 L276 60" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M276 60 L267.7 63.5 L267.7 56.5 Z" style={{ fill: "var(--faint)" }} />
  <path d="M276 110 L244 110" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M244 110 L252.3 106.5 L252.3 113.5 Z" style={{ fill: "var(--faint)" }} />
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2s" repeatCount="indefinite" path="M240 60 L276 60 M276 110 L244 110" />
  </circle>
</svg>
<figcaption>Status codes in the 200s mean success. 400s mean the request was wrong. 500s mean the server failed.</figcaption>
</figure>

The lab below uses all four methods.

      <input placeholder="New book title" value={title} onChange={(e) => setTitle(e.target.value)} />{" "}
      <button onClick={addBook}>Add</button>
      <ul>
        {books.map((book) => (
          <li key={book.id}>
            {book.title} · ₹{book.price}{" "}
            <button onClick={() => raisePrice(book)}>+₹50</button>{" "}
            <button onClick={() => removeBook(book.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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.

```tsx
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.

```tsx
// 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:

```tsx
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**:

<div className="tscroll">

| 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" |

</div>

      <button onClick={() => load("/books/1")}>Book 1</button>{" "}
      <button onClick={() => load("/books/99")}>Book 99</button>{" "}
      <button onClick={() => load("/error")}>Broken server</button>
      <p>{result}</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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.

```tsx
{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.

<figure className="react-fig">
<svg viewBox="0 0 520 150" role="img" aria-labelledby="fig-p-states" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-states">Loading states: idle, then loading, then either success with data or an error</title>
  <rect x="5" y="50" width="100" height="46" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="55.0" y="70.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>idle</text>
  <text x="55.0" y="87.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>nothing yet</text>
  <path d="M105 73 L140 73" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M140 73 L131.7 76.5 L131.7 69.5 Z" style={{ fill: "var(--faint)" }} />
  <text x="122.5" y="62" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>fetch</text>
  <rect x="145" y="50" width="110" height="46" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="200.0" y="70.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>loading</text>
  <text x="200.0" y="87.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>show spinner</text>
  <path d="M255 62 L330 30" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M330 30 L323.8 36.5 L321.0 30.0 Z" style={{ fill: "var(--faint)" }} />
  <text x="292.5" y="36" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>ok</text>
  <rect x="335" y="8" width="175" height="46" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="422.5" y="28.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>success</text>
  <text x="422.5" y="45.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>show the data</text>
  <path d="M255 84 L330 118" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M330 118 L321.0 117.8 L323.9 111.4 Z" style={{ fill: "var(--faint)" }} />
  <text x="292.5" y="110" textAnchor="middle" style={{ fill: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)" }}>fails</text>
  <rect x="335" y="96" width="175" height="46" rx="8" style={{ fill: "var(--bad-soft)", stroke: "var(--bad)", strokeWidth: 1.5 }} />
  <text x="422.5" y="116.0" textAnchor="middle" style={{ fill: "var(--bad)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>error</text>
  <text x="422.5" y="133.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>show a message</text>
  <circle r="6" style={{ fill: "var(--ok)" }}>
    <animateMotion dur="2.2s" repeatCount="indefinite" path="M105 73 L140 73 M255 62 L330 30" />
  </circle>
</svg>
<figcaption>Every API call goes through these states. Plan what the screen shows in each one.</figcaption>
</figure>

- **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.

      <label>
        <input type="checkbox" checked={broken} onChange={(e) => setBroken(e.target.checked)} /> Break the server
      </label>

      {isLoading && <p>Loading books...</p>}

      {!isLoading && error && (
        <p className="error">
          {error} <button onClick={load}>Try again</button>
        </p>
      )}

      {!isLoading && !error && (
        <ul>{books.map((b) => <li key={b.id}>{b.title}</li>)}</ul>
      )}
    </div>
  );
}
`,
    "App.css": `.error { color: #dc2626; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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()`.

<figure className="react-fig">
<svg viewBox="0 0 520 110" role="img" aria-labelledby="fig-p-layer" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-layer">Service layer: components call bookService, which uses one API client, which talks to the server</title>
  <rect x="5" y="30" width="120" height="50" rx="8" style={{ fill: "var(--accent-soft)", stroke: "var(--accent)", strokeWidth: 1.5 }} />
  <text x="65.0" y="52.0" textAnchor="middle" style={{ fill: "var(--accent)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Components</text>
  <text x="65.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>show the screen</text>
  <path d="M125 55 L155 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M155 55 L146.7 58.5 L146.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="160" y="30" width="120" height="50" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="220.0" y="52.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>bookService</text>
  <text x="220.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>getBooks()</text>
  <path d="M280 55 L310 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M310 55 L301.7 58.5 L301.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="315" y="30" width="90" height="50" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="360.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>apiClient</text>
  <text x="360.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>fetch helper</text>
  <path d="M405 55 L435 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M435 55 L426.7 58.5 L426.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="440" y="30" width="75" height="50" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="477.5" y="60.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>API</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2.4s" repeatCount="indefinite" path="M125 55 L155 55 M280 55 L310 55 M405 55 L435 55" />
  </circle>
</svg>
<figcaption>Each layer has one job. Components never build URLs themselves.</figcaption>
</figure>

- **apiClient.ts** — one small helper around `fetch`. It adds the base address, sends JSON, checks `response.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.

      <button onClick={handleAdd}>Add Ikigai</button>
      <ul>
        {books.map((b) => (
          <li key={b.id}>
            {b.title} by {b.author} <button onClick={() => handleDelete(b.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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:

```bash
VITE_API_URL=https://api.bookcorner.dev
```

Then you read them in your code with `import.meta.env`:

```tsx
const response = await fetch(import.meta.env.VITE_API_URL + "/books");
```

<figure className="react-fig">
<svg viewBox="0 0 520 110" role="img" aria-labelledby="fig-p-env" style={{ width: "100%", maxWidth: 520, height: "auto" }}>
  <title id="fig-p-env">Environment variables: the .env file is read by Vite, and your code reads the value with import.meta.env</title>
  <rect x="5" y="25" width="170" height="60" rx="8" style={{ fill: "var(--panel2)", stroke: "var(--faint)", strokeWidth: 1.5 }} />
  <text x="90.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>.env</text>
  <text x="90.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>VITE_API_URL=https://...</text>
  <path d="M175 55 L205 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M205 55 L196.7 58.5 L196.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="210" y="25" width="100" height="60" rx="8" style={{ fill: "var(--accent-secondary-soft)", stroke: "var(--accent-secondary)", strokeWidth: 1.5 }} />
  <text x="260.0" y="52.0" textAnchor="middle" style={{ fill: "var(--accent-secondary)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Vite</text>
  <text x="260.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>reads it at start</text>
  <path d="M310 55 L340 55" style={{ stroke: "var(--faint)", strokeWidth: 2, fill: "none" }} />
  <path d="M340 55 L331.7 58.5 L331.7 51.5 Z" style={{ fill: "var(--faint)" }} />
  <rect x="345" y="25" width="170" height="60" rx="8" style={{ fill: "var(--ok-soft)", stroke: "var(--ok)", strokeWidth: 1.5 }} />
  <text x="430.0" y="52.0" textAnchor="middle" style={{ fill: "var(--ok)", fontSize: 13, fontWeight: 700, fontFamily: "var(--mono)" }}>Your code</text>
  <text x="430.0" y="69.0" textAnchor="middle" style={{ fill: "var(--ink2)", fontSize: 11, fontFamily: "var(--sans)" }}>import.meta.env.VITE_API_URL</text>
  <circle r="6" style={{ fill: "var(--accent-secondary)" }}>
    <animateMotion dur="2s" repeatCount="indefinite" path="M175 55 L205 55 M310 55 L340 55" />
  </circle>
</svg>
<figcaption>Change the .env file, not the code, when the address changes.</figcaption>
</figure>

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.

      <h3>{import.meta.env.VITE_SHOP_NAME}</h3>
      <p>API address: {import.meta.env.VITE_API_URL}</p>
      <p>Mode: {import.meta.env.MODE}</p>
      <p>{count === null ? "Loading..." : count + " books on the server"}</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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`: the `User` type and `NewUser` (a user without an id).
- `apiClient.ts`: one `request()` helper around `fetch`.
- `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.tsx` and `UserForm.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**.

```ts
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.

      {users.map((u) => (
        <li key={u.id}>
          <b>{u.name}</b> · {u.email} · {u.role}
        </li>
      ))}
    </ul>
  );
}
`,
    "apiClient.ts": `const BASE_URL = "https://api.bookcorner.dev";

// For the lesson: send every request to a broken address, to see error handling
let brokenServer = false;
  brokenServer = on;
}

  constructor(message: string, public status: number) {
    super(message);
  }
}

// Turn a status code into a message a user understands
function friendly(status: number): string {
  if (status === 400) return "Some details are not valid. Please check the form.";
  if (status === 404) return "This user was not found. It may have been deleted.";
  if (status >= 500) return "The server has a problem. Please try again.";
  return "Something went wrong. Please try again.";
}

// One helper for every request: JSON in, JSON out, errors as ApiError
  const url = BASE_URL + (brokenServer ? "/error" : path);
  let response: Response;
  try {
    response = await fetch(url, {
      method: options.method ?? "GET",
      headers: { "Content-Type": "application/json" },
      body: options.body === undefined ? undefined : JSON.stringify(options.body),
    });
  } catch {
    throw new ApiError("Cannot reach the server. Check your internet connection.", 0);
  }
  if (!response.ok) throw new ApiError(friendly(response.status), response.status);
  if (response.status === 204) return undefined as T;
  return response.json();
}
`,
    "userService.ts": `import { request } from "./apiClient";

// The API service layer: components call these, never fetch directly
  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" }),
};
`,
    "types.ts": `export type Role = "Admin" | "Editor" | "Viewer";

  id: number;
  name: string;
  email: string;
  role: Role;
}

// A user before the server gives it an id
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

### 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:

<div className="tscroll">

| 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 |

</div>

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**.

      <h2>User Management</h2>
      <div className="toolbar">
        <span>{users.length} users</span>
        <button onClick={reload}>Reload</button>
        <label>
          <input type="checkbox" checked={broken} onChange={(e) => { setBroken(e.target.checked); setBrokenServer(e.target.checked); }} />
          Simulate a server error
        </label>
      </div>

      {error && (
        <div className="alert">
          <span>{error}</span>
          <button onClick={clearError}>Dismiss</button>
        </div>
      )}

      {loading ? (
        <p className="loading">Loading users...</p>
      ) : (
      )}

      <UserForm users={users} editing={editing} saving={saving} onSave={handleSave} onCancel={() => setEditing(null)} />
    </div>
  );
}
`,
    "useUsers.ts": `import { useCallback, useEffect, useState } from "react";

// All user data and CRUD actions in one hook. Components stay simple.
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState("");

  const load = useCallback(async () => {
    setLoading(true);
    setError("");
    try {
      setUsers(await userService.getAll());
    } catch (e) {
      setError((e as Error).message);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    load();
  }, [load]);

  // Runs one save action with the "saving" state and error handling.
  // Returns true when it worked, so the form knows whether to reset.
  async function run(action: () => Promise<void>): Promise<boolean> {
    setSaving(true);
    setError("");
    try {
      await action();
      return true;
    } catch (e) {
      setError((e as Error).message);
      return false;
    } finally {
      setSaving(false);
    }
  }

  return {
    users,
    loading,
    saving,
    error,
    reload: load,
    clearError: () => setError(""),
    add: (user: NewUser) =>
      run(async () => {
        const created = await userService.create(user);
        setUsers((list) => [...list, created]);
      }),
    edit: (user: User) =>
      run(async () => {
        const updated = await userService.update(user);
        setUsers((list) => list.map((u) => (u.id === updated.id ? updated : u)));
      }),
    remove: (id: number) =>
      run(async () => {
        await userService.remove(id);
        setUsers((list) => list.filter((u) => u.id !== id));
      }),
  };
}
`,
    "UserTable.tsx": `import { useState } from "react";

interface UserTableProps {
  users: User[];
  saving: boolean;
  onEdit: (user: User) => void;
  onDelete: (id: number) => void;
}

  // Which row is asking "Delete? Yes / No"
  const [confirmId, setConfirmId] = useState<number | null>(null);

  if (users.length === 0) return <p className="empty">No users yet. Add the first one.</p>;

  return (
    <table className="users">
      <thead>
        <tr><th>Name</th><th>Email</th><th>Role</th><th /></tr>
      </thead>
      <tbody>
        {users.map((u) => (
          <tr key={u.id} className="user-row">
            <td className="cell-name">{u.name}</td>
            <td>{u.email}</td>
            <td><span className={"role " + u.role}>{u.role}</span></td>
            <td className="actions">
              {confirmId === u.id ? (
                <>
                  Delete?{" "}
                  <button className="yes" disabled={saving} onClick={() => { onDelete(u.id); setConfirmId(null); }}>Yes</button>{" "}
                  <button onClick={() => setConfirmId(null)}>No</button>
                </>
              ) : (
                <>
                  <button className="edit" onClick={() => onEdit(u)}>Edit</button>{" "}
                  <button className="delete" onClick={() => setConfirmId(u.id)}>Delete</button>
                </>
              )}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
`,
    "UserForm.tsx": `import { useEffect, useState, type FormEvent } from "react";

interface UserFormProps {
  users: User[];
  editing: User | null;
  saving: boolean;
  onSave: (user: NewUser) => Promise<boolean>;
  onCancel: () => void;
}

const empty: NewUser = { name: "", email: "", role: "Viewer" };

  const [form, setForm] = useState<NewUser>(empty);
  const [errors, setErrors] = useState<UserErrors>({});

  // When "Edit" is clicked, fill the form with that user's details
  useEffect(() => {
    setForm(editing ? { name: editing.name, email: editing.email, role: editing.role } : empty);
    setErrors({});
  }, [editing]);

  async function handleSubmit(e: FormEvent) {
    e.preventDefault();
    const found = validateUser(form, users, editing?.id);
    setErrors(found);
    if (Object.keys(found).length > 0) return;
    const ok = await onSave({ ...form, name: form.name.trim(), email: form.email.trim() });
    if (ok && !editing) setForm(empty);
  }

  return (
    <form className="user-form" onSubmit={handleSubmit} noValidate>
      <h3>{editing ? "Edit user" : "Add a new user"}</h3>
      <label>
        Name
        <input className="name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
      </label>
      {errors.name && <p className="field-error">{errors.name}</p>}
      <label>
        Email
        <input className="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} />
      </label>
      {errors.email && <p className="field-error">{errors.email}</p>}
      <label>
        Role
        <select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value as Role })}>
          <option>Admin</option>
          <option>Editor</option>
          <option>Viewer</option>
        </select>
      </label>
      <div>
        <button type="submit" className="save" disabled={saving}>
          {saving ? "Saving..." : editing ? "Save changes" : "Add user"}
        </button>{" "}
        {editing && <button type="button" onClick={onCancel}>Cancel</button>}
      </div>
    </form>
  );
}
`,
    "validateUser.ts": `import type { NewUser, User } from "./types";

  const errors: UserErrors = {};
  const name = user.name.trim();
  const email = user.email.trim().toLowerCase();

  if (name.length === 0) errors.name = "Please enter a name.";
  else if (name.length < 3) errors.name = "The name needs at least 3 characters.";

  if (email.length === 0) errors.email = "Please enter an email.";
  else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) errors.email = "Please enter a valid email, like name@example.com.";
  else if (others.some((u) => u.email.toLowerCase() === email && u.id !== editingId)) errors.email = "Another user already has this email.";

  return errors;
}
`,
    "apiClient.ts": `const BASE_URL = "https://api.bookcorner.dev";

// For the lesson: send every request to a broken address, to see error handling
let brokenServer = false;
  brokenServer = on;
}

  constructor(message: string, public status: number) {
    super(message);
  }
}

// Turn a status code into a message a user understands
function friendly(status: number): string {
  if (status === 400) return "Some details are not valid. Please check the form.";
  if (status === 404) return "This user was not found. It may have been deleted.";
  if (status >= 500) return "The server has a problem. Please try again.";
  return "Something went wrong. Please try again.";
}

// One helper for every request: JSON in, JSON out, errors as ApiError
  const url = BASE_URL + (brokenServer ? "/error" : path);
  let response: Response;
  try {
    response = await fetch(url, {
      method: options.method ?? "GET",
      headers: { "Content-Type": "application/json" },
      body: options.body === undefined ? undefined : JSON.stringify(options.body),
    });
  } catch {
    throw new ApiError("Cannot reach the server. Check your internet connection.", 0);
  }
  if (!response.ok) throw new ApiError(friendly(response.status), response.status);
  if (response.status === 204) return undefined as T;
  return response.json();
}
`,
    "userService.ts": `import { request } from "./apiClient";

// The API service layer: components call these, never fetch directly
  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" }),
};
`,
    "types.ts": `export type Role = "Admin" | "Editor" | "Viewer";

  id: number;
  name: string;
  email: string;
  role: Role;
}

// A user before the server gives it an id
`,
    "styles.css": `.app { font-family: sans-serif; max-width: 640px; }
.app h2 { margin: 0 0 8px; color: #2b59c3; }
.toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; font-size: 13px; }
.alert { background: #fde8e8; color: #b42318; padding: 8px 10px; border-radius: 6px; display: flex; justify-content: space-between; gap: 8px; }
.loading { color: #2b59c3; }
.users { width: 100%; border-collapse: collapse; font-size: 14px; }
.users th, .users td { text-align: left; padding: 6px; border-bottom: 1px solid #eee; }
.actions { white-space: nowrap; }
.role { padding: 1px 8px; border-radius: 999px; font-size: 12px; }
.role.Admin { background: #fde2e2; } .role.Editor { background: #fff3cd; } .role.Viewer { background: #e3f6e8; }
.user-form { display: grid; gap: 6px; background: #f6f8fc; padding: 10px; border-radius: 8px; margin-top: 12px; }
.user-form h3 { margin: 0; }
.user-form label { display: grid; gap: 2px; font-size: 13px; font-weight: bold; }
.user-form input, .user-form select { padding: 5px; }
.field-error { color: #b42318; font-size: 12px; margin: 0; }
.empty { color: #666; }
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

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](/downloads/react/user-management-project.zip)

```
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

1. Install **Node.js** 18 or newer from nodejs.org, if you do not have it.
2. Download the ZIP with the link above, and unzip it. You get a folder called `user-management`.
3. Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
4. Install the packages and start the app and the API together:

```bash
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

<div className="tscroll">

| 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 |

</div>

## Check Yourself

## Exercises

## What to Remember

- An API is how your app asks a server for data. Requests are async, so use `async` and `await`.
- `fetch` is built into the browser. Check `response.ok`, and call `response.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
