# nuxt-aweasome-http

[![npm version](https://img.shields.io/npm/v/nuxt-aweasome-http.svg?style=flat-square&color=00DC82)](https://www.npmjs.com/package/nuxt-aweasome-http)
[![npm downloads](https://img.shields.io/npm/dm/nuxt-aweasome-http.svg?style=flat-square&color=00DC82)](https://www.npmjs.com/package/nuxt-aweasome-http)
[![npm total downloads](https://img.shields.io/npm/dt/nuxt-aweasome-http.svg?style=flat-square&color=00DC82)](https://www.npmjs.com/package/nuxt-aweasome-http)
[![license](https://img.shields.io/npm/l/nuxt-aweasome-http.svg?style=flat-square&color=00DC82)](https://github.com/AlphaDelta38/nuxt-aweasome-http/blob/main/LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/AlphaDelta38/nuxt-aweasome-http?style=flat-square&color=00DC82)](https://github.com/AlphaDelta38/nuxt-aweasome-http)

📋 **[Changelog / Patch Notes](./CHANGELOG.md)**

Vue 3 / Nuxt composable for typed HTTP requests with SSR support, IndexedDB caching, smart loading, and global interceptors.

## Features

- 🚀 **SSR Ready**: Automatically fetches data on the server side and hydrates on the client without double-fetching. (Hydrated data is automatically saved to local cache for instant future navigations!).
- 💾 **IndexedDB Cache**: Extremely fast local caching. Instant UI feedback while fetching fresh data in the background (stale-while-revalidate).
- 😴 **Lazy Loading**: Manual control over when requests are dispatched.
- 🛠️ **Global Settings & Interceptors**: Modify requests globally, handle auth tokens, or process responses at the core level.
- 🔒 **Type-Safe**: Designed with TypeScript in mind.

## Installation

Install the package with your favorite package manager:

```bash
# npm
npm install nuxt-aweasome-http

# pnpm
pnpm add nuxt-aweasome-http

# yarn
yarn add nuxt-aweasome-http

# bun
bun add nuxt-aweasome-http
```

Then, register the module in your `nuxt.config.ts`:

```typescript
export default defineNuxtConfig({
  modules: [
    'nuxt-aweasome-http'
  ]
})
```

*(If you are developing this package locally, it is auto-imported based on your Nuxt configuration).*

## Basic Usage

The composable `useHttp` handles all your fetching states: `data`, `isLoading`, `isCacheLoading`, `error`.

```vue
<script setup lang="ts">
import { useHttp } from 'nuxt-aweasome-http'

const {
  data,
  isLoading,
  isCacheLoading,
  isFreshData,
  error,
  fetch: refetch,
} = useHttp('GET: https://jsonplaceholder.typicode.com/todos', {
  ssr: true,           // Enable Server-Side Rendering
  cache: true,         // Enable IndexedDB cache
  lazy: false,         // Set to true to prevent fetching on mount
  ttl: 30000,          // Cache Time-To-Live in ms
  // componentKey: 'my-custom-key', // Optional: Auto-generated by default.
  initOptions: {
    query: {
      _limit: 5,
    }
  }
})
</script>

<template>
  <div v-if="isLoading && !data">Loading from network...</div>
  <div v-else-if="isCacheLoading && !data">Loading from cache...</div>
  <div v-else-if="error">Error occurred!</div>
  
  <div v-else>
    <ul>
      <li v-for="todo in data" :key="todo.id">
        {{ todo.title }}
      </li>
    </ul>
    
    <!-- When using cache, you get instant data but it might load fresh data in the background -->
    <div v-if="isLoading && data">Updating in background...</div>
  </div>
</template>
```

### State Sharing & `componentKey`

The `componentKey` is generated automatically, so you usually don't need to specify it. However, because `useHttp` uses Nuxt's `useState` for the response data, if you manually provide the *same* `componentKey` to multiple components, they will automatically share the exact same reactive `data`! (Note: Fetching states like `isLoading` and `error` remain local to each individual component).

### The `effect` Callback

The `effect` callback fires every time data arrives — whether from cache or from the network. This is the recommended place to handle side effects (updating external state, tracking metrics, etc.), especially when using cache.

```typescript
const { data } = useHttp('GET: /api/todos', {
  cache: true,
  effect(data, config) {
    // config.cache — true if data came from IndexedDB, false if from network
    // config.isServer — true if running on the server (SSR)
    // config.params — the params used for this request
    // config.query — the query used for this request

    if (config.cache) {
      console.log('⚡ Instant cache hit!')
    } else {
      console.log('🌐 Fresh data from network')
    }
  },
})
```

The `effect` callback signature:

| Parameter | Type | Description |
|-----------|------|-------------|
| `data` | `T` | The response data (from cache or network). |
| `config.cache` | `boolean` | `true` if this data came from IndexedDB cache. |
| `config.isServer` | `boolean` | `true` if currently executing on the server (SSR). |
| `config.params` | `object` | Route params used for this request. |
| `config.query` | `object` | Query parameters used for this request. |

### Re-fetching with `fetch` & `useCache`

The `fetch` method returned by `useHttp` allows you to re-request data with new parameters. It accepts an object with two fields:

| Field | Type | Description |
|-------|------|-------------|
| `options` | `RequestOptions` | New `query`, `params`, and other fetch options for this request. |
| `useCache` | `boolean` | If `true`, check IndexedDB cache first before making a network request. |

> **Important:** `fetch()` always returns **fresh data from the network** as its `Promise` result. However, when `useCache: true` is set, the composable will **first** update `data` with the cached value (and fire `effect` with `config.cache = true`), and **then** make a network request to get fresh data (firing `effect` again with `config.cache = false`). This is the stale-while-revalidate pattern.
>
> Because of this two-step flow, if you need to react to both cached and fresh data (e.g., for tracking load times), **use the `effect` callback** — not the `await` result of `fetch()`.

**Pagination example** — switching pages without remounting the component:

```typescript
const cacheElapsed = ref<number | null>(null)
const networkElapsed = ref<number | null>(null)
let startTime = Date.now()

const { data, fetch: refetch } = useHttp('GET: /api/todos', {
  cache: true,
  ttl: 30000,
  initOptions: { query: { _start: 0, _limit: 10 } },
  effect(data, config) {
    if (config.cache) {
      cacheElapsed.value = Date.now() - startTime   // ⚡ Instant
    } else {
      networkElapsed.value = Date.now() - startTime  // 🌐 After network
    }
  },
})

// When the user clicks "Next Page":
async function goToPage(page: number) {
  startTime = Date.now()
  cacheElapsed.value = null
  networkElapsed.value = null

  await refetch({
    useCache: true,  // 1) Show cached page instantly  2) Update from network
    options: {
      query: { _start: (page - 1) * 10, _limit: 10 },
    },
  })
}

// Without cache (always wait for network):
await refetch({
  options: { query: { _start: 20, _limit: 10 } },
})
```

### Automatic Request Cancellation

`useHttp` features built-in race-condition protection. If you trigger `fetch()` while a previous request is still pending, the module **automatically aborts the previous request** at the network level using an internal `AbortController`. This saves bandwidth and ensures your `data` and `isLoading` states remain perfectly in sync with the latest request.

If you need to manually cancel a request, you can pass your own `signal`:

```typescript
const controller = new AbortController()

await refetch({
  options: { signal: controller.signal }
})

// Manually abort the request
controller.abort()
```

## Global Settings & Interceptors

You can configure global settings for all your requests using `initAweasomeHttp`. This is especially useful for setting base URLs, adding authentication tokens, or globally handling errors (e.g., 401 Unauthorized redirects).

It is recommended to set this up in a Nuxt plugin or at the root of your application (`app.vue`).

```typescript
import { initAweasomeHttp } from 'nuxt-aweasome-http'

if (import.meta.client) {
  initAweasomeHttp({
    baseUrl: 'https://api.my-domain.com/v1',
    interceptors: {
      // Intercept request before it is sent
      request: async (ctx) => {
        // Example: Add an Authorization header
        // const token = useCookie('auth-token').value
        // if (token) {
        //   ctx.options.headers = {
        //     ...ctx.options.headers,
        //     Authorization: `Bearer ${token}`
        //   }
        // }
        
        // Example: Add artificial delay for testing
        // await new Promise(resolve => setTimeout(resolve, 1500))
        
        return ctx
      },
      // Intercept response before returning it to the composable
      response: (response) => {
        if (response.status === 401) {
          // e.g. Redirect to login
        }
        return response
      }
    }
  })
}
```

### Clearing the Cache

If you need to manually clear the IndexedDB cache (for example, when a user logs out), you can use the `clearCache` utility.

```typescript
import { clearCache } from 'nuxt-aweasome-http'

async function logout() {
  await clearCache()
  // ... other logout logic
}
```

## Direct API Requests (`request`)

Sometimes you may want to perform a simple HTTP request without the composable state overhead (e.g., in a Vuex/Pinia store, outside a Vue component, or when you don't need reactivity like `isLoading`). 
For these cases, you can use the standalone `request` function. It supports the same strict typing and global interceptors.

```typescript
import { request } from 'nuxt-aweasome-http'

async function fetchMyData() {
  try {
    const data = await request('GET: https://jsonplaceholder.typicode.com/todos/:id', {
      params: { id: 1 }
    })
    console.log('Got data:', data) // Strictly typed as Todo!
  } catch (err) {
    console.error('Request failed', err)
  }
}
```

## TypeScript & Type Declarations

Because this package is strictly typed, you can get full auto-completion for your API endpoints and responses. **You cannot just pass a generic type to `useHttp` or `request`** — the module strictly enforces your URL strings based on a central source of truth!

You have two ways to define your API types:

### 1. Auto-Generated Conventions (Recommended)

For mid-to-large applications, `nuxt-aweasome-http` offers a powerful **auto-generating types system**. Instead of manually typing every request, you define your endpoints once in `*.convention.ts` files. The module will automatically stitch them together into the global scope.

When you use `useHttp` (or the standalone `request` function), your IDE will instantly auto-complete URLs, enforce correct `query`/`params`, and strongly type the `data` response!

#### Configuration

By default, the module looks for files ending in `*.convention.ts` inside the `conventions/` folder at the root of your project, with a depth of `1`. 

You can customize this in your `nuxt.config.ts` to fit your project's architecture (for example, Domain-Driven Design or a deeper nested structure):

```typescript
export default defineNuxtConfig({
  modules: ['nuxt-aweasome-http'],
  
  aweasomeHttp: {
    // You can disable auto-generation entirely if you prefer manual typings
    // autoGenerateConventions: false,

    // Example 1: Classic Structure
    // Will search in /conventions/*.convention.ts
    conventionsDir: 'conventions', 
    conventionsDepth: 1,
    
    // Example 2: Domain-Driven Design (DDD)
    // Will search up to 3 levels deep: /domains/users/api/users.convention.ts
    // conventionsDir: 'domains',
    // conventionsDepth: 3,
  }
})
```

#### Writing a Convention File

Inside any `*.convention.ts` file, simply **default export** an interface containing your endpoints. 

Here is an example structure:

```text
📦 my-nuxt-app
 ┣ 📂 domains
 ┃ ┗ 📂 users
 ┃   ┗ 📂 api
 ┃     ┗ 📜 users.convention.ts   <-- Your convention file
 ┣ 📜 nuxt.config.ts
```

```typescript
// domains/users/api/users.convention.ts

export interface User {
  id: number
  name: string
  email: string
}

// MUST be exported as default!
export default interface UserEndpoints {
  // Pattern: "METHOD: URL"
  'GET: https://api.example.com/users': {
    query: { _limit?: number, _sort?: string }
    data: User[]
  }
  
  // Dynamic parameters are supported (e.g. :id)
  'GET: https://api.example.com/users/:id': {
    query: {} // Explicitly require no query params
    data: User
  }
  
  'POST: https://api.example.com/users': {
    query: {}
    data: { success: boolean; id: number }
  }
}
```

That's it! When you run `nuxt dev`, the module will automatically inject these types globally.

### 2. Manual Global Declaration (Alternative)

If you don't want to use the auto-generation feature (or if you have a very small app), you can disable it and manually extend the global `Convention` interface.

First, disable auto-generation in your config:
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  aweasomeHttp: {
    autoGenerateConventions: false
  }
})
```

Then, in any `.d.ts` file (e.g., `types/index.d.ts`):

```typescript
// types/index.d.ts
export interface Todo {
  userId: number
  id: number
  title: string
  completed: boolean
}

declare global {
  interface Convention {
    'GET: https://api.example.com/todos': {
      query: { _limit?: number }
      data: Todo[]
    }
  }
}

// Must be a module!
export {}
```

### Exported Types

If you are building wrappers around this module, you can import its native type definitions directly:

```typescript
import type { 
  ComposableResponse, 
  RequestOptions, 
  Settings 
} from 'nuxt-aweasome-http'
```
