npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@geekmidas/client

v0.0.1

Published

Type-safe client library for consuming HTTP APIs with full TypeScript support, React Query integration, and automatic code generation from OpenAPI specifications.

Downloads

147

Readme

@geekmidas/client

Type-safe client library for consuming HTTP APIs with full TypeScript support, React Query integration, and automatic code generation from OpenAPI specifications.

Features

  • Type-Safe Fetcher: Fully typed HTTP client with automatic type inference
  • React Query Integration: Pre-built hooks with TypeScript support
  • OpenAPI Code Generation: Generate React Query hooks from OpenAPI specs
  • Infinite Queries: Built-in support for pagination and infinite scroll
  • Automatic Validation: Request/response validation with StandardSchema
  • Error Handling: Type-safe error handling with HTTP status codes
  • Query Invalidation: Type-safe cache invalidation
  • Method Restrictions: Type-level enforcement of HTTP methods per endpoint

Installation

pnpm add @geekmidas/client

For React Query integration:

pnpm add @geekmidas/client @tanstack/react-query

Package Exports

// Type-safe fetcher
import { createTypedFetcher } from '@geekmidas/client';

// React Query client
import { createTypedQueryClient } from '@geekmidas/client/react-query';

// OpenAPI hooks generation
import { generateReactQueryHooks } from '@geekmidas/client/openapi';

// Type utilities
import type { TypedFetcherOptions } from '@geekmidas/client/types';

Quick Start

Type-Safe Fetcher

Create a typed fetcher for your API:

import { createTypedFetcher } from '@geekmidas/client';

// Define your API types
interface API {
  'GET /users': {
    response: {
      id: string;
      name: string;
      email: string;
    }[];
  };
  'POST /users': {
    body: {
      name: string;
      email: string;
    };
    response: {
      id: string;
      name: string;
      email: string;
    };
  };
  'GET /users/:id': {
    params: {
      id: string;
    };
    response: {
      id: string;
      name: string;
      email: string;
    };
  };
}

// Create typed fetcher
const api = createTypedFetcher<API>({
  baseUrl: 'https://api.example.com'
});

// Use with full type safety
const users = await api('GET /users');
// users is typed as Array<{ id: string; name: string; email: string }>

const user = await api('POST /users', {
  body: { name: 'John Doe', email: '[email protected]' }
});
// user is typed as { id: string; name: string; email: string }

const singleUser = await api('GET /users/:id', {
  params: { id: '123' }
});
// singleUser is typed as { id: string; name: string; email: string }

React Query Integration

Use with React Query for automatic caching and state management:

import { createTypedQueryClient } from '@geekmidas/client/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// Create query client
const queryClient = new QueryClient();
const api = createTypedQueryClient<API>({
  baseUrl: 'https://api.example.com'
});

// In your component
function UsersList() {
  const { data, isLoading, error } = api.useQuery('GET /users');

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Mutations
function CreateUser() {
  const createUser = api.useMutation('POST /users');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await createUser.mutateAsync({
      body: { name: 'Jane Doe', email: '[email protected]' }
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button disabled={createUser.isPending}>
        Create User
      </button>
    </form>
  );
}

Query Parameters

Handle query parameters with full type safety:

interface API {
  'GET /users/search': {
    query: {
      q: string;
      limit?: number;
      offset?: number;
    };
    response: {
      users: Array<{ id: string; name: string }>;
      total: number;
    };
  };
}

// Usage
const result = await api('GET /users/search', {
  query: { q: 'john', limit: 10 }
});

Infinite Queries

Implement infinite scroll pagination:

function InfiniteUsersList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = api.useInfiniteQuery(
    'GET /users',
    {
      query: { limit: 20 }
    },
    {
      getNextPageParam: (lastPage, pages) => {
        if (pages.length * 20 < lastPage.total) {
          return { offset: pages.length * 20 };
        }
        return undefined;
      }
    }
  );

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.users.map(user => (
            <div key={user.id}>{user.name}</div>
          ))}
        </div>
      ))}
      {hasNextPage && (
        <button
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          Load More
        </button>
      )}
    </div>
  );
}

OpenAPI Code Generation

Generate React Query hooks from OpenAPI specifications:

# Using CLI
pnpm gkm generate:react-query --input api-docs.json --output ./src/api

# Programmatic usage
import { generateReactQueryHooks } from '@geekmidas/client/openapi';
import fs from 'fs/promises';

const spec = JSON.parse(await fs.readFile('api-docs.json', 'utf-8'));
const code = await generateReactQueryHooks(spec);
await fs.writeFile('./src/api/generated.ts', code);

Generated hooks example:

// Generated from OpenAPI spec
export const api = createTypedQueryClient<{
  'GET /users': {
    response: User[];
  };
  'POST /users': {
    body: CreateUserRequest;
    response: User;
  };
  // ... all your endpoints
}>({
  baseUrl: process.env.REACT_APP_API_URL
});

// Use generated hooks
function MyComponent() {
  const { data: users } = api.useQuery('GET /users');
  const createUser = api.useMutation('POST /users');

  return (
    // Your component
  );
}

Advanced Features

Query Invalidation

Type-safe cache invalidation:

// Invalidate specific query
await api.invalidateQueries('GET /users');

// Invalidate with params
await api.invalidateQueries('GET /users/:id', {
  params: { id: '123' }
});

// Invalidate multiple queries
await Promise.all([
  api.invalidateQueries('GET /users'),
  api.invalidateQueries('GET /users/:id')
]);

Optimistic Updates

Implement optimistic UI updates:

const updateUser = api.useMutation('PUT /users/:id', {
  onMutate: async (variables) => {
    // Cancel outgoing refetches
    await api.cancelQueries('GET /users/:id', {
      params: { id: variables.params.id }
    });

    // Snapshot previous value
    const previousUser = api.getQueryData('GET /users/:id', {
      params: { id: variables.params.id }
    });

    // Optimistically update
    api.setQueryData('GET /users/:id', {
      params: { id: variables.params.id }
    }, variables.body);

    return { previousUser };
  },
  onError: (err, variables, context) => {
    // Rollback on error
    if (context?.previousUser) {
      api.setQueryData('GET /users/:id', {
        params: { id: variables.params.id }
      }, context.previousUser);
    }
  },
  onSettled: (data, error, variables) => {
    // Refetch after mutation
    api.invalidateQueries('GET /users/:id', {
      params: { id: variables.params.id }
    });
  }
});

Custom Headers

Add custom headers to requests:

const api = createTypedFetcher<API>({
  baseUrl: 'https://api.example.com',
  headers: {
    'Authorization': `Bearer ${token}`,
    'X-Api-Key': apiKey
  }
});

// Per-request headers
const user = await api('GET /users/:id', {
  params: { id: '123' },
  headers: {
    'X-Request-ID': requestId
  }
});

Error Handling

Handle errors with full type safety:

import { HttpError } from '@geekmidas/errors';

try {
  const user = await api('GET /users/:id', {
    params: { id: '123' }
  });
} catch (error) {
  if (error instanceof HttpError) {
    if (error.statusCode === 404) {
      console.log('User not found');
    } else if (error.statusCode === 403) {
      console.log('Access denied');
    } else {
      console.error('API error:', error.message);
    }
  } else {
    console.error('Unexpected error:', error);
  }
}

Request/Response Interceptors

Add interceptors for logging, auth, etc:

const api = createTypedFetcher<API>({
  baseUrl: 'https://api.example.com',
  beforeRequest: async (url, options) => {
    // Add auth token
    const token = await getAuthToken();
    options.headers = {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    };
    return { url, options };
  },
  afterResponse: async (response) => {
    // Log response
    console.log(`${response.status} ${response.url}`);
    return response;
  },
  onError: async (error) => {
    // Handle auth errors
    if (error instanceof HttpError && error.statusCode === 401) {
      await refreshAuthToken();
      // Retry request
    }
    throw error;
  }
});

Prefetching

Prefetch queries for better UX:

function UsersList() {
  const { data: users } = api.useQuery('GET /users');

  const handleUserHover = (userId: string) => {
    // Prefetch user details on hover
    api.prefetchQuery('GET /users/:id', {
      params: { id: userId }
    });
  };

  return (
    <ul>
      {users?.map(user => (
        <li
          key={user.id}
          onMouseEnter={() => handleUserHover(user.id)}
        >
          {user.name}
        </li>
      ))}
    </ul>
  );
}

Type Utilities

Infer API Types

import type { InferAPIResponse, InferAPIRequest } from '@geekmidas/client/types';

type UsersResponse = InferAPIResponse<API, 'GET /users'>;
// type UsersResponse = Array<{ id: string; name: string; email: string }>

type CreateUserRequest = InferAPIRequest<API, 'POST /users'>;
// type CreateUserRequest = { body: { name: string; email: string } }

Method Restrictions

Enforce correct HTTP methods at type level:

// ✅ Correct - POST endpoint with body
await api('POST /users', {
  body: { name: 'John' }
});

// ❌ Type error - GET endpoint can't have body
await api('GET /users', {
  body: { name: 'John' } // Type error!
});

// ❌ Type error - Wrong method
await api('DELETE /users', {}); // Type error if endpoint not defined!

Testing

Mock API calls in tests:

import { createTypedFetcher } from '@geekmidas/client';
import { vi } from 'vitest';

const mockFetch = vi.fn();
global.fetch = mockFetch;

const api = createTypedFetcher<API>({
  baseUrl: 'https://api.example.com'
});

// Mock response
mockFetch.mockResolvedValueOnce({
  ok: true,
  json: async () => [{ id: '1', name: 'John', email: '[email protected]' }]
});

const users = await api('GET /users');
expect(users).toHaveLength(1);
expect(mockFetch).toHaveBeenCalledWith(
  'https://api.example.com/users',
  expect.objectContaining({ method: 'GET' })
);

Related Packages

License

MIT