Example Requests

Basic usage of the grab library

Install & Import

npm install grab-url
bun i grab-url
import grab from "grab-url";

Basic Request

// Basic GET request
grab("user").then(log); // { id: 123, name: "John Doe", ... }

// GET with query parameters
const searchResults = await grab("search", {
  query: "javascript",
  page: 1,
  limit: 10,
});

// POST with body
await grab("users", {
  post: true, //shorthand for method: "POST"
  email: "jane@example.com",
  age: 25,
});

Reactive Loading Status

import React, { useState } from "react";
import grab from "grab-url";

function UserProfile() {
  const [userState, setUserState] = useState<Partial<{ 
    name: string; 
    email: string;
    isLoading: boolean;
    error: string; 
  }>>({})

  await grab(`user`, {
    response: setUserState
  });

  return (
    <div>
      {userState.isLoading && <div>Loading...</div>}
      {userState.error && <div>Error: {userState.error}</div>}
      {userState && (
        <div>
          <h2>{userState.name}</h2>
          <p>{userState.email}</p>
        </div>
      )}
    </div>
  );
}
<script>
  import grab from 'grab-url';


  let searchResults = $state({
    results: [],
    isLoading: false,
    error: null
  });

  async function searchProducts(query) {
    await grab('products/search', {
      response: searchResults,
      post: true,
      query: query,
      category: 'electronics'
    });
  }
</script>

<input
  type="text"
  on:input={(e) => searchProducts(e.target.value)}
  placeholder="Search products..."
/>

{#if searchResults.isLoading}
  <div class="loading">Searching...</div>
{:else if searchResults.error}
  <div class="error">{searchResults.error}</div>
{:else if searchResults.results}
  <div class="results">
    {#each searchResults.results as product}
      <div class="product-card">
        <h3>{product.name}</h3>
        <p>${product.price}</p>
      </div>
    {/each}
  </div>
{/if}
<template>
  <div>
    <input 
      v-model="searchTerm" 
      @input="searchUsers"
      placeholder="Search users..."
    />
    <div v-if="userResults.isLoading" class="loading">
      Loading users...
    </div>
    <div v-else-if="userResults.error" class="error">
      {{ userResults.error }}
    </div>
    <div v-else class="user-list">
      <div 
        v-for="user in userResults.users" 
        :key="user.id"
        class="user-card"
      >
        {{ user.name }} - {{ user.email }}
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import grab from 'grab-url';
const searchTerm = ref('');
const userResults = reactive({
  users: [],
  isLoading: false,
  error: null
});
const searchUsers = async () => {
  if (searchTerm.value.length < 2) return;
  
  await grab('users/search', {
    response: userResults,
    query: searchTerm.value,
    status: 'active'
  });
};
</script>

TypeScript ToolTips & Error Check on Request & Response

type User = { 
  /** Name of the user  */ 
  name: string;
  /** Age of the user  */ 
  age: number;
};

type RequestParams = { 
  /** Query String  to search for */ 
  q : string;
  /** Category of search  */
  category?: "news" | "general";

};

const result = await grab<User, RequestParams>('test-path', {
  q: " react",
  category: "general"
});

log(result.name)

// instant error highlight and description tooltips over "category" and "name"

Pagination with Infinite Scroll

let productList = $state({}) as {
  products: Array<{name:string}>,
  isLoading: boolean
}

await grab("products", {
  response: productList,
  infiniteScroll: ["page", "products", ".results-container"]
});

<div class="results-container">
  {productList.products.map(product => (
    <div className="product-item">
      <h3>{product.name}</h3>
    </div>
  ))}
  {productList.isLoading && <div className="loading">Loading more products...</div>}
</div>
// Seconds to debounce request, wait to execute so that other requests may override

await grab("search", {
  response: searchResults,
  debounce: 2,
  query: query,
  cancelOngoingIfNew: true, // Cancel previous searches
});

Client Cache in User Memory

// Enable caching for static data
const categories = await grab("categories", {
  cache: true,
});

const categoriesAgain = await grab("categories", {
  cache: true,
}); 
// Instant response from frontend cache, no server call

Rate Limiting

// Prevent user's multi-click cascading requests
let searchResults = {};

async function searchWithRateLimit(query) {
  await grab("search", {
    response: searchResults,
    query: query,
    rateLimit: 2, // Wait 2 seconds between requests
  });
}
searchWithRateLimit("python"); // Executes immediately
searchWithRateLimit("golang"); // fails, needs to wait

Duplicate Cancellation

let currentSearch = {};

async function searchProducts(query) {
  // Cancel previous search when new one starts
  await grab("products/search", {
    response: currentSearch,
    query: query,
    cancelOngoingIfNew: true, // Default behavior
  });
}

// Prevent new requests if one is ongoing
async function preventDuplicateRequests(userId) {
  await grab(`users/${userId}`, {
    cancelNewIfOngoing: true, // Prevents duplicate requests
  });
}

Error Handling and Retry

let apiData = {};

// Automatic retry on failure
await grab("unreliable-endpoint", {
  response: apiData,
  retryAttempts: 3, // Retry 3 times on failure
  timeout: 10, // 10 second timeout
});

// Manual error handling
try {
  const result = await grab("api/data");
  console.log("Success:", result);
} catch (error) {
  if (error.message.includes("timeout")) {
    console.log("Request timed out");
  } else if (error.message.includes("rate limit")) {
    console.log("Too many requests");
  } else {
    console.log("Other error:", error.message);
  }
}

Request Hooks and Interceptors

// Global request interceptor
grab.defaults.onRequest = (path, response, params, fetchParams) => {
  // Add authentication header
  fetchParams.headers.Authorization = `Bearer ervv0sf9vs0v0sv`;

  // Modify request data
  if (params.userId) {
    params.user_id = '2525';
  }

  return [path, response, params, fetchParams];
};

Proxy

//polyfill fetch with node-fetch
import fetch, { Headers, Request, Response } from 'node-fetch';

if (!globalThis.fetch) {
  globalThis.fetch = fetch;
  globalThis.Headers = Headers;
  globalThis.Request = Request;
  globalThis.Response = Response;
}

//get proxy
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://username:password@proxyhost:port");

let res = await grab("/path", { agent });

File Upload

const formData = new FormData();
formData.append("file", file);

const response = await grab("/api/upload", {
  post: true,
  body: formData,
  headers: {
    Authorization: `Bearer ${getAuthToken()}`,
  },
});

// Alternative: Convert file to base64 for JSON API
async function uploadFileAsJSON(file) {
  const base64 = await fileToBase64(file);

  return await grab("files/upload", {
    post: true,
    filename: file.name,
    content: base64,
    mimeType: file.type,
  });
}

function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result.split(",")[1]);
    reader.onerror = (error) => reject(error);
  });
}

Last updated on

On this page