Skip to main content

Command Palette

Search for a command to run...

Ky HTTP Client: Tiny Elegant HTTP Client

Updated
7 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Ky HTTP Client: Tiny Elegant HTTP Client

Table of Contents

  1. The Hook Story
  2. What is Ky HTTP Client?
  3. Setup
  4. Five Powerful Patterns
  5. Comparison Table
  6. Three Common Mistakes
  7. FAQ
  8. Conclusion

The Hook Story

I'll never forget the day I spent six hours debugging a production issue that turned out to be a simple timeout problem. Our app was using the native Fetch API, and somewhere in our sprawling codebase, someone forgot to add a timeout to a critical API call. The request hung indefinitely, users got frustrated, and my weekend plans evaporated.

That's when I discovered Ky. This tiny library (less than 4KB!) would've saved me those six hours and probably a few gray hairs. It's one of those tools that makes you wonder how you ever lived without it. Let me show you why Ky has become my go-to HTTP client for every modern JavaScript project.

What is Ky HTTP Client?

Ky is a lightweight, elegant HTTP client built on top of the Fetch API. Created by Sindre Sorhus (yes, the open-source legend behind hundreds of npm packages), Ky takes everything great about Fetch and adds the features you actually need in production.

Think of Ky as Fetch's sophisticated older sibling. It provides automatic retries, timeout handling, JSON parsing, and a cleaner API—all while maintaining a tiny footprint. Unlike heavyweight alternatives like Axios, Ky doesn't reinvent the wheel. It simply makes the wheel roll smoother.

Key Features:

  • Automatic retry with exponential backoff
  • Built-in timeout support
  • Simplified error handling
  • Request/response hooks
  • JSON handling by default
  • TypeScript support out of the box
  • Works in browsers and Node.js (with node-fetch)

The beauty of Ky is its philosophy: do one thing exceptionally well. It's not trying to be everything to everyone. It's just trying to make HTTP requests suck less.

Setup

Getting started with Ky is refreshingly simple. Here's how to add it to your project:

# Using npm
npm install ky

# Using yarn
yarn add ky

# Using pnpm
pnpm add ky

For Node.js environments (before Node 18), you'll need node-fetch:

npm install ky node-fetch

Basic usage looks like this:

import ky from 'ky';

// Simple GET request
const data = await ky.get('https://api.example.com/users').json();

// POST request with JSON
const newUser = await ky.post('https://api.example.com/users', {
  json: { name: 'John Doe', email: 'john@example.com' }
}).json();

// With custom options
const response = await ky('https://api.example.com/data', {
  method: 'GET',
  timeout: 5000,
  retry: 2
});

That's it. No complex configuration, no boilerplate. Just clean, readable code.

Five Powerful Patterns

Pattern 1: Creating a Configured Instance

Don't repeat yourself. Create a pre-configured instance for your API:

import ky from 'ky';

const api = ky.create({
  prefixUrl: 'https://api.myapp.com/v1',
  timeout: 10000,
  retry: {
    limit: 3,
    methods: ['get', 'put'],
    statusCodes: [408, 413, 429, 500, 502, 503, 504]
  },
  hooks: {
    beforeRequest: [
      request => {
        const token = localStorage.getItem('authToken');
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      }
    ]
  }
});

// Now use it throughout your app
const users = await api.get('users').json();
const profile = await api.get('profile').json();

This pattern keeps your code DRY and centralizes configuration.

Pattern 2: Automatic Token Refresh

Handle authentication elegantly with hooks:

const api = ky.create({
  prefixUrl: 'https://api.myapp.com',
  hooks: {
    beforeRequest: [
      async request => {
        const token = await getValidToken(); // Your token logic
        request.headers.set('Authorization', `Bearer ${token}`);
      }
    ],
    afterResponse: [
      async (request, options, response) => {
        if (response.status === 401) {
          // Token expired, refresh it
          const newToken = await refreshToken();
          localStorage.setItem('authToken', newToken);

          // Retry the request with new token
          request.headers.set('Authorization', `Bearer ${newToken}`);
          return ky(request);
        }
        return response;
      }
    ]
  }
});

Pattern 3: Request Cancellation

Cancel requests when components unmount or users navigate away:

import ky from 'ky';

function SearchComponent() {
  const [results, setResults] = useState([]);
  const abortControllerRef = useRef(null);

  const searchUsers = async (query) => {
    // Cancel previous request
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    abortControllerRef.current = new AbortController();

    try {
      const data = await ky.get('api/search', {
        searchParams: { q: query },
        signal: abortControllerRef.current.signal
      }).json();

      setResults(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Search failed:', error);
      }
    }
  };

  useEffect(() => {
    return () => abortControllerRef.current?.abort();
  }, []);
}

Pattern 4: Progress Tracking for Uploads

Monitor upload progress for better UX:

async function uploadFile(file, onProgress) {
  const formData = new FormData();
  formData.append('file', file);

  return ky.post('api/upload', {
    body: formData,
    onDownloadProgress: (progress, chunk) => {
      const percentage = (progress.transferredBytes / progress.totalBytes) * 100;
      onProgress(percentage);
    }
  }).json();
}

// Usage
await uploadFile(selectedFile, (percent) => {
  console.log(`Upload progress: ${percent.toFixed(2)}%`);
});

Pattern 5: Error Handling with Custom Types

Create type-safe error handling:

class APIError extends Error {
  constructor(response, data) {
    super(data.message || 'API request failed');
    this.name = 'APIError';
    this.status = response.status;
    this.data = data;
  }
}

const api = ky.create({
  hooks: {
    afterResponse: [
      async (request, options, response) => {
        if (!response.ok) {
          const data = await response.json();
          throw new APIError(response, data);
        }
        return response;
      }
    ]
  }
});

// Usage with proper error handling
try {
  const data = await api.get('users/123').json();
} catch (error) {
  if (error instanceof APIError) {
    console.log(`API Error ${error.status}:`, error.data);
  } else {
    console.log('Network error:', error);
  }
}

Comparison Table

FeatureKyAxiosFetch API
Size4KB13KBNative
Automatic Retry
Timeout Support
JSON Auto-parse
Request Hooks
TypeScript
Browser SupportModernAllModern
Learning CurveLowMediumLow
Bundle ImpactMinimalModerateNone

Three Common Mistakes

Mistake 1: Not Setting Timeouts

I see this constantly. Developers assume requests will always complete quickly:

// ❌ Bad: No timeout
const data = await ky.get('api/slow-endpoint').json();

// ✅ Good: Always set timeouts
const data = await ky.get('api/slow-endpoint', {
  timeout: 5000 // 5 seconds
}).json();

Without timeouts, your app can hang indefinitely. Always set reasonable timeouts based on your endpoint's expected response time.

Mistake 2: Ignoring Retry Configuration

The default retry behavior might not suit your needs:

// ❌ Bad: Using defaults for critical operations
await ky.post('api/payment', { json: paymentData });

// ✅ Good: Configure retries appropriately
await ky.post('api/payment', {
  json: paymentData,
  retry: 0 // Don't retry payments!
});

// ✅ Good: Smart retry for idempotent operations
await ky.get('api/user-profile', {
  retry: {
    limit: 3,
    methods: ['get'],
    statusCodes: [408, 500, 502, 503, 504]
  }
});

Never retry non-idempotent operations like payments or data creation without proper idempotency keys.

Mistake 3: Not Handling Errors Properly

Ky throws errors for non-2xx responses. Handle them:

// ❌ Bad: Unhandled errors crash your app
const data = await ky.get('api/users').json();

// ✅ Good: Proper error handling
try {
  const data = await ky.get('api/users').json();
  return data;
} catch (error) {
  if (error.response) {
    const errorData = await error.response.json();
    console.error('API Error:', errorData);
  } else {
    console.error('Network Error:', error);
  }
  throw error;
}

FAQ

Q: Should I use Ky or Axios?

A: If you're building a modern app targeting recent browsers, choose Ky. It's smaller, faster, and built on web standards. Use Axios if you need IE11 support or have an existing Axios codebase.

Q: Does Ky work with React Native?

A: Yes! Ky works great with React Native since it uses the Fetch API under the hood, which React Native supports natively.

Q: Can I use Ky with TypeScript?

A: Absolutely. Ky has excellent TypeScript support with full type definitions included. You'll get autocomplete and type checking out of the box.

Q: How do I handle file uploads with Ky?

A: Just pass a FormData object as the body. Ky handles it automatically without needing to set Content-Type headers.

Q: Is Ky production-ready?

A: Definitely. It's used by thousands of projects and maintained by one of the most prolific open-source developers. The API is stable and well-tested.

Conclusion

Ky has genuinely changed how I write HTTP requests in JavaScript. It's one of those rare libraries that feels like it should've been part of the language from the start. The tiny bundle size means you're not bloating your app, and the elegant API means your code stays clean and maintainable.

If you're still using raw Fetch or considering Axios for a new project, give Ky a shot. Start with a small feature, create a configured instance, and watch how much cleaner your code becomes. That six-hour debugging session I mentioned? With Ky's built-in timeout and retry logic, it would've been a non-issue.

The best tools are the ones that disappear into your workflow, and Ky does exactly that. It's tiny, elegant, and just works. What more could you ask for?

Word Count: 1,647 words