# HTTP Requests

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Working with APIs
Source: https://www.devtutorials.ai/learn/vue/apis/http-requests

> HTTP, REST and JSON in plain words, fetch() for GET, POST, PUT, PATCH and DELETE with headers, bodies and query strings, response.ok and status codes, loading, success, empty and error states, refetching, stale responses and AbortController, Axios instances, interceptors and cancellation, a full CRUD screen, API service modules and env-based URLs.

## Talking to a Server

- Real apps keep their data on a **server**: menus, orders, users.
- The browser asks the server for data, and sends new data to it, over **HTTP**.
- **Request** = a message from the browser: "give me the dishes".
- **Response** = the server's answer: a status code and some data.
- **JSON** = the text format most servers use for data. It looks like JavaScript objects.

**Simple lesson:** the app sends HTTP requests, and the server answers with JSON.

### A Real-World Example

- Meera's menu now lives on a server.
- The page must show "Loading…" first, then the dishes.
- If the server fails, it must show a friendly message, not a blank page.

interface Dish {
    id: number;
    name: string;
    price: number;
}

const dishes = ref<Dish[]>([]);
const loading = ref<boolean>(true);
const error = ref<string | null>(null);

onMounted(async () => {
    try {
      const res = await fetch("https://api.meeraskitchen.dev/dishes");
      if (!res.ok) throw new Error(\`Server error \${res.status}\`);
      dishes.value = (await res.json()) as Dish[];
    } catch (e) {
      error.value = (e as Error).message;
    } finally {
      loading.value = false;
    }
});
</script>

<template>
    <p v-if="loading">Loading the menu…</p>
    <p v-else-if="error">Sorry, the menu could not load. ({{ error }})</p>
    <p v-else-if="dishes.length === 0">No dishes today.</p>
    <ul v-else>
      <li v-for="d in dishes" :key="d.id">{{ d.name }}: ₹{{ d.price }}</li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "Loading the menu…" showed first.
- About 300 ms later the dishes arrived, and the list replaced it.
- The **Console** below the preview shows the request: `[API] GET /dishes → 200`.

Real apps call a real server over the internet. In this lab, a pretend REST API at `https://api.meeraskitchen.dev` answers inside the preview. It has `/dishes`, `/orders`, `/staff` and `/reviews`, and `/error` always fails. Add `?delay=1500` to any address to make it slower.

### What You Will Learn

1. What HTTP methods, REST and status codes mean.
2. How to send GET, POST, PUT, PATCH and DELETE with `fetch()`.
3. How to add headers, a body and a query string.
4. Why you must check `response.ok`.
5. How to show loading, success, empty and error states.
6. How to load again and cancel old requests.
7. How to use Axios, its instances and interceptors.
8. How to build a full create, read, update and delete screen.
9. How to keep API calls in one module and the address in `.env`.

## HTTP and REST in Plain Words

- **REST** = a common way to design a server's addresses around things, such as dishes.
- The **method** says what to do with the thing.

<div className="tscroll">

| Method | Meaning | Example |
|---|---|---|
| `GET` | Read | `GET /dishes`, `GET /dishes/2` |
| `POST` | Create | `POST /dishes` with a new dish |
| `PUT` | Replace all of it | `PUT /dishes/2` with the full dish |
| `PATCH` | Change part of it | `PATCH /dishes/2` with `{ price: 95 }` |
| `DELETE` | Remove | `DELETE /dishes/2` |

</div>

### Status Codes

- **2xx** = success. `200` OK, `201` Created, `204` No content.
- **4xx** = the request was wrong. `400` Bad request, `401` Not logged in, `403` Not allowed, `404` Not found.
- **5xx** = the server failed. `500` Server error.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-vue-http" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-http">Animation: the browser sends POST /dishes with a JSON body to the server. The server answers 201 Created with the new dish and its id</title>
  <rect x="20" y="40" width="160" height="110" rx="14" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="100" y="100" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 15, fontWeight: 700 }}>Vue app</text>
  <rect x="460" y="40" width="160" height="110" rx="14" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="540" y="100" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 15, fontWeight: 700 }}>Server</text>
  <path d="M180 75 H458" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <path d="M460 120 H182" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <g style={{ fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>
    <g><animateTransform attributeName="transform" type="translate" values="0 0;270 0;270 0" keyTimes="0;0.4;1" dur="6s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.4;0.41;1" dur="6s" repeatCount="indefinite" /><rect x="190" y="58" width="180" height="34" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} /><text x="280" y="80" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>POST /dishes {"{ name }"}</text></g>
    <g opacity="0"><animateTransform attributeName="transform" type="translate" values="0 0;0 0;-270 0;-270 0" keyTimes="0;0.5;0.9;1" dur="6s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.51;0.9;0.95" dur="6s" repeatCount="indefinite" /><rect x="270" y="103" width="180" height="34" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="360" y="125" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>201 {"{ id: 5, name }"}</text></g>
  </g>
  <text x="320" y="180" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>Request: method + address + body. Response: status code + JSON.</text>
</svg>
<figcaption>Every request gets a response with a status code.</figcaption>
</figure>

**Simple lesson:** the method says what to do. The status code says how it went.

## fetch() for Every Method

- `fetch(url)` sends a GET and returns a **Promise** of the response.
- `await res.json()` reads the JSON body.
- For other methods, pass options: `method`, `headers` and `body`.
- Send JSON with `JSON.stringify(data)` and the header `Content-Type: application/json`.
- Build a query string with `URLSearchParams`: `?veg=true`.

const API = "https://api.meeraskitchen.dev";
const out = ref<string>("Press a button and open the Console.");

async function send(method: string, path: string, body?: object): Promise<void> {
    const res = await fetch(API + path, {
      method,
      headers: { "Content-Type": "application/json" },
      body: body ? JSON.stringify(body) : undefined,
    });
    const data = await res.json();
    out.value = \`\${method} \${path} → \${res.status}: \${JSON.stringify(data)}\`;
}

function getVeg(): Promise<void> {
    const query = new URLSearchParams({ veg: "true" });
    return send("GET", \`/dishes?\${query}\`);
}
</script>

<template>
    <button @click="send('GET', '/dishes/2')">GET one</button>
    <button @click="getVeg">GET with query</button>
    <button @click="send('POST', '/dishes', { name: 'Pongal', price: 70, veg: true })">POST</button>
    <button @click="send('PUT', '/dishes/1', { name: 'Ghee Dosa', price: 110, veg: true })">PUT</button>
    <button @click="send('PATCH', '/dishes/1', { price: 95 })">PATCH</button>
    <button @click="send('DELETE', '/dishes/4')">DELETE</button>
    <pre style="white-space: pre-wrap">{{ out }}</pre>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## Checking response.ok

- `fetch` only fails for **network** problems, such as no internet.
- A `404` or `500` is still a "successful" fetch. It does not throw.
- So always check `res.ok` (true for 2xx) or `res.status`.
- Throw your own error when it is not ok.

const message = ref<string>("");

async function load(path: string): Promise<void> {
    try {
      const res = await fetch("https://api.meeraskitchen.dev" + path);
      if (!res.ok) {
        const body = (await res.json()) as { message?: string };
        throw new Error(\`\${res.status}: \${body.message ?? "Request failed"}\`);
      }
      const data = await res.json();
      message.value = "OK: " + JSON.stringify(data).slice(0, 60);
    } catch (e) {
      message.value = "Error " + (e as Error).message;
    }
}
</script>

<template>
    <button @click="load('/dishes/1')">Dish 1 (200)</button>
    <button @click="load('/dishes/99')">Dish 99 (404)</button>
    <button @click="load('/error')">Broken (500)</button>
    <p>{{ message }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Without the `res.ok` check, a 404 page or an error object goes straight into your list. The page then shows strange values or crashes. Check `ok` on every request.

**Simple lesson:** `fetch` does not throw on 404 or 500. Check `res.ok` yourself.

## Four States of a Request

- **Loading:** the request is on its way. Show a spinner or "Loading…".
- **Success:** data arrived. Show it.
- **Empty:** it worked, but there is nothing. Say so clearly.
- **Error:** it failed. Explain it kindly and offer "Try again".
- The first box on this page shows all four.

## Refetching and Stale Responses

- Data often loads again: a new filter, a new page, a "Refresh" button.
- If the user clicks fast, an **old** slow request can finish **after** a newer one.
- The page then shows old data. This is a **stale response**.
- Fix: cancel the old request with an `AbortController` before starting a new one.
- Also cancel in `onUnmounted`, as you did in **Lifecycle and Template Refs**.

const filter = ref<string>("all");
const names = ref<string>("");

watch(
    filter,
    async (f) => {
      const controller = new AbortController();
      onWatcherCleanup(() => controller.abort());
      const delay = f === "veg" ? 1500 : 300;
      const query = f === "veg" ? \`?veg=true&delay=\${delay}\` : \`?delay=\${delay}\`;
      try {
        const res = await fetch(\`https://api.meeraskitchen.dev/dishes\${query}\`, { signal: controller.signal });
        const list = (await res.json()) as { name: string }[];
        names.value = \`\${f}: \${list.map((d) => d.name).join(", ")}\`;
      } catch (e) {
        if ((e as Error).name === "AbortError") console.log("Cancelled the", f, "request");
      }
    },
    { immediate: true },
);
</script>

<template>
    <button @click="filter = 'veg'">Veg (slow)</button>
    <button @click="filter = 'all'">All (fast)</button>
    <p>{{ names }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** click "Veg (slow)", then quickly "All (fast)".
- The slow veg request was cancelled, so it could not overwrite the newer list.

**Simple lesson:** cancel old requests with `AbortController`, so new data always wins.

## Axios

- **Axios** = a popular library for HTTP requests. Install it with `npm install axios`.
- It reads JSON for you: `res.data`.
- It **throws** on 4xx and 5xx, so you do not forget the check.
- `axios.create({ baseURL, headers, timeout })` makes an **instance** with shared settings.
- **Interceptors** run on every request or response: add a token, log, handle errors in one place.
- Cancel with the same `AbortController`: `{ signal: controller.signal }`.

interface Dish {
    id: number;
    name: string;
}

const text = ref<string>("");

async function loadDishes(): Promise<void> {
    const res = await api.get<Dish[]>("/dishes", { params: { veg: true } });
    text.value = res.data.map((d) => d.name).join(", ");
}

async function loadBroken(): Promise<void> {
    try {
      await api.get("/error");
    } catch (e) {
      text.value = (e as Error).message;
    }
}
</script>

<template>
    <button @click="loadDishes">Veg dishes with Axios</button>
    <button @click="loadBroken">Broken request</button>
    <p>{{ text }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Axios in a browser normally sends requests with `XMLHttpRequest`. In this lab, Axios is set to its built-in `fetch` adapter, so the pretend API can answer. Your code does not change.

## CORS and the Dev Proxy

- **CORS** = a browser rule. A page from one address may only read answers from **another** address if that server allows it.
- If it does not, the request fails with a "blocked by CORS policy" error in the browser Console. This is the server's setting, not a bug in your Vue code.
- Fix on the server: it must send `Access-Control-Allow-Origin` for your site.
- During development, Vite can forward requests for you, so the browser sees one address:

```ts
// vite.config.ts
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: { "/api": "http://localhost:8080" }, // fetch("/api/dishes") → http://localhost:8080/api/dishes
  },
});
```

- The proxy works only in `npm run dev`. In production, the server or the host must allow it.

## A Full CRUD Screen

- **CRUD** = Create, Read, Update, Delete. Most admin screens do all four.
- Keep the HTTP calls in a **service module**, such as `services/dishes.ts`.
- Components then call `dishService.list()`, not raw URLs.
- Keep the server address in `.env` as `VITE_API_URL`, read with `import.meta.env`. You will learn more in **Errors, Security and Deployment**.

const dishes = ref<Dish[]>([]);
const name = ref<string>("");
const price = ref<number>(60);
const error = ref<string>("");

async function refresh(): Promise<void> {
    dishes.value = await dishService.list();
}

async function add(): Promise<void> {
    if (!name.value) return;
    await dishService.create({ name: name.value, price: price.value });
    name.value = "";
    await refresh();
}

async function raise(d: Dish): Promise<void> {
    await dishService.update({ ...d, price: d.price + 10 });
    await refresh();
}

async function remove(id: number): Promise<void> {
    try {
      await dishService.remove(id);
      await refresh();
    } catch (e) {
      error.value = (e as Error).message;
    }
}

onMounted(refresh);
</script>

<template>
    <form @submit.prevent="add">
      <input v-model.trim="name" placeholder="New dish" />
      <input v-model.number="price" type="number" />
      <button>Add</button>
    </form>
    <ul>
      <li v-for="d in dishes" :key="d.id">
        {{ d.name }} ₹{{ d.price }}
        <button @click="raise(d)">+₹10</button>
        <button @click="remove(d.id)">Delete</button>
      </li>
    </ul>
    <p>{{ error }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

For bigger apps, wrap the service in a composable or a Pinia store, as in **Composables, Directives and Plugins** and **State Management with Pinia**. Then every page shares the same loading and error state.

**Simple lesson:** keep HTTP calls in a service module, and the server address in `.env`.

## TL;DR

- **HTTP** = requests and responses between the app and a server.
- **REST methods** = GET read, POST create, PUT replace, PATCH change, DELETE remove.
- **Status codes** = 2xx success, 4xx bad request, 5xx server failure.
- **`fetch(url, { method, headers, body })`** = sends a request. `await res.json()` reads JSON.
- **`res.ok`** = true for 2xx. `fetch` does not throw on 404 or 500.
- **Four states** = loading, success, empty, error.
- **`AbortController`** = cancels a request. Stops stale responses.
- **Axios** = `res.data`, throws on errors, instances and interceptors.
- **Service module + `.env`** = one place for API calls and the server address.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No `res.ok` check | `fetch` throws on 404 | Check `res.ok` and throw | Error objects end up in your list |
| Forgetting `JSON.stringify` | `body: data` works | `body: JSON.stringify(data)` | The server gets `[object Object]` |
| No `Content-Type` header | The server guesses JSON | Send `Content-Type: application/json` | The server ignores the body |
| No loading or error state | Requests are instant | Show loading, empty and error | A blank or broken page |
| Not cancelling old requests | Answers arrive in order | Abort the old one | Old data replaces new data |
| URLs scattered in components | Fine for small apps | A service module and `.env` | Changing the server means editing every file |
| Secrets in `VITE_` variables | `.env` is private | Keep secrets on the server | Anyone can read them in the browser |

</div>

## Check Yourself

## Exercises

## What to Remember

- GET reads, POST creates, PUT replaces, PATCH changes, DELETE removes.
- `fetch` does not throw on 404 or 500. Always check `res.ok`.
- Show loading, success, empty and error states.
- Cancel old requests with `AbortController` to avoid stale data.
- Keep API calls in a service module, and the server address in `.env`.

Next: **Authentication and Real-Time Data**
