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 🙏

© 2026 – Pkg Stats / Ryan Hefner

gen-query

v1.8.0

Published

Query with tanstack using generics

Readme

gen-query

npm version npm downloads License Nuxt

A powerful Nuxt module for type-safe API queries using TanStack Query (Vue Query) with full TypeScript support and automatic cache management.

Features

  • 🔥 Type-safe - Full TypeScript support with generics
  • 🚀 TanStack Query - Built on TanStack Query for powerful data fetching and caching
  • 🎯 Composables-first - Easy-to-use Vue composables for all operations
  • 📄 Pagination - Built-in infinite scroll and pagination support
  • 🔄 Smart Caching - Automatic cache invalidation with configurable update strategies
  • 🔐 Authentication - Optional token-based authentication
  • 🎨 Flexible Filtering - Advanced filtering with multiple match modes
  • 📦 Lightweight - Minimal dependencies

Quick Setup

Install the module:

npx nuxi module add gen-query

Configuration

Configure in nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['gen-query'],

  genQuery: {
    baseURL: 'https://api.example.com', // API base URL
    cachedPages: 4, // Max cached pages for infinite queries
    update: UpdateStrategy.Invalidate, // Cache update strategy
  },
})

Configuration Options

| Option | Type | Default | Description | | ------------- | ---------------- | ------------ | ------------------------------------------------------------------------------------------ | | baseURL | string | '' | Base URL for all API requests. Can also be set via NUXT_PUBLIC_API_BASE_URL env variable | | cachedPages | number | 4 | Maximum number of pages to cache in infinite queries | | update | UpdateStrategy | Invalidate | Strategy for updating cache after mutations |

Update Strategies

The module supports three cache update strategies:

  • UpdateStrategy.None - No automatic cache updates. You manage cache manually.
  • UpdateStrategy.Invalidate - Invalidates and refetches affected queries after mutations (recommended).
  • UpdateStrategy.Optimistic - Immediately updates cache optimistically before server confirms.
import { UpdateStrategy } from 'gen-query'

export default defineNuxtConfig({
  genQuery: {
    update: UpdateStrategy.Optimistic, // Use optimistic updates
  },
})

Quick Start

<script setup lang="ts">
  import type { Entity } from 'gen-query'

  // Define your entity type
  interface Product extends Entity<number> {
    name: string
    price: number
  }

  // Fetch all products
  const productQuery = useMultipleQuery<Product, number>('products')

  // Create a new product
  productQuery.create.mutate({ name: 'New Product', price: 99.99 })
</script>

<template>
  <div v-if="productQuery.read.isLoading.value">Loading...</div>
  <div v-else>
    <div v-for="product in productQuery.read.data.value" :key="product.id">
      {{ product.name }} - ${{ product.price }}
    </div>
  </div>
</template>

Core Concepts

Composables

gen-query provides four main composables:

  1. useLoginService - Handle user authentication
  2. useSingleQuery - CRUD operations for a single entity by ID
  3. useMultipleQuery - CRUD operations for collections
  4. usePaginatedQuery - Paginated data with filtering and sorting

All composables return TanStack Query objects with reactive properties for data, loading states, errors, and mutation functions.

Queries vs Mutations

  • Queries (read, list, page) - Fetch data with automatic caching and background refetching
  • Mutations (create, update, del) - Modify data with automatic cache updates based on your strategy

Usage Guide

Authentication

Use useLoginService for user authentication:

<script setup lang="ts">
  import type { Login, User } from 'gen-query'

  const loginService = useLoginService('auth') // 'auth' is the resource endpoint

  const credentials: Login = {
    username: '[email protected]',
    password: 'password123',
  }

  const handleLogin = () => {
    loginService.login.mutate(credentials, {
      onSuccess: (user: User) => {
        console.log('Logged in:', user.token)
        // Store token for subsequent requests
        localStorage.setItem('token', user.token)
      },
      onError: (error) => {
        console.error('Login failed:', error.message)
      },
    })
  }
</script>

<template>
  <form @submit.prevent="handleLogin">
    <button type="submit" :disabled="loginService.login.isPending.value">
      {{ loginService.login.isPending.value ? 'Logging in...' : 'Login' }}
    </button>
    <div v-if="loginService.login.isError.value" class="error">
      {{ loginService.login.error.value?.message }}
    </div>
  </form>
</template>

Single Entity Operations

Use useSingleQuery to work with a single entity by ID:

<script setup lang="ts">
  import type { Entity } from 'gen-query'

  interface Product extends Entity<number> {
    name: string
    price: number
    category: string
    description: string
  }

  const productId = ref(1)
  const token = ref(localStorage.getItem('token') || undefined)

  const productQuery = useSingleQuery<Product, number>('products', productId, token)

  const handleUpdate = () => {
    if (!productQuery.read.data.value) return

    productQuery.update.mutate(
      {
        ...productQuery.read.data.value,
        price: productQuery.read.data.value.price * 0.9, // 10% discount
      },
      {
        onSuccess: () => {
          console.log('Product updated!')
        },
      }
    )
  }

  const handleDelete = () => {
    if (!productQuery.read.data.value) return

    productQuery.del.mutate(productQuery.read.data.value, {
      onSuccess: () => {
        console.log('Product deleted!')
        // Navigate away or update UI
      },
    })
  }
</script>

<template>
  <div v-if="productQuery.read.isLoading.value">Loading...</div>
  <div v-else-if="productQuery.read.isError.value">
    Error: {{ productQuery.read.error.value?.message }}
  </div>
  <div v-else-if="productQuery.read.data.value">
    <h1>{{ productQuery.read.data.value.name }}</h1>
    <p>{{ productQuery.read.data.value.description }}</p>
    <p>Price: ${{ productQuery.read.data.value.price }}</p>

    <button @click="handleUpdate" :disabled="productQuery.update.isPending.value">
      {{ productQuery.update.isPending.value ? 'Updating...' : 'Apply 10% Discount' }}
    </button>

    <button @click="handleDelete" :disabled="productQuery.del.isPending.value">
      {{ productQuery.del.isPending.value ? 'Deleting...' : 'Delete Product' }}
    </button>
  </div>
</template>

Collection Operations

Use useMultipleQuery for CRUD operations on collections:

<script setup lang="ts">
  import type { Entity } from 'gen-query'

  interface Product extends Entity<number> {
    name: string
    price: number
    category: string
  }

  const token = ref(localStorage.getItem('token') || undefined)
  const productQuery = useMultipleQuery<Product, number>('products', token)

  const newProduct = ref({ name: '', price: 0, category: '' })

  const handleCreate = () => {
    productQuery.create.mutate(newProduct.value, {
      onSuccess: (created) => {
        console.log('Created:', created)
        newProduct.value = { name: '', price: 0, category: '' }
      },
    })
  }

  const handleUpdate = (product: Product) => {
    productQuery.update.mutate({
      ...product,
      price: product.price * 1.1, // 10% increase
    })
  }

  const handleDelete = (product: Product) => {
    if (confirm(`Delete ${product.name}?`)) {
      productQuery.del.mutate(product)
    }
  }
</script>

<template>
  <div>
    <!-- Create Form -->
    <form @submit.prevent="handleCreate">
      <input v-model="newProduct.name" placeholder="Name" required />
      <input v-model.number="newProduct.price" type="number" placeholder="Price" required />
      <input v-model="newProduct.category" placeholder="Category" required />
      <button type="submit" :disabled="productQuery.create.isPending.value">
        {{ productQuery.create.isPending.value ? 'Creating...' : 'Create Product' }}
      </button>
    </form>

    <!-- Product List -->
    <div v-if="productQuery.read.isLoading.value">Loading products...</div>
    <div v-else-if="productQuery.read.isError.value">
      Error: {{ productQuery.read.error.value?.message }}
    </div>
    <div v-else>
      <div v-for="product in productQuery.read.data.value" :key="product.id" class="product-card">
        <h3>{{ product.name }}</h3>
        <p>Price: ${{ product.price }}</p>
        <p>Category: {{ product.category }}</p>

        <button @click="handleUpdate(product)" :disabled="productQuery.update.isPending.value">
          Increase Price 10%
        </button>
        <button @click="handleDelete(product)" :disabled="productQuery.del.isPending.value">
          Delete
        </button>
      </div>

      <button @click="productQuery.read.refetch()" :disabled="productQuery.read.isFetching.value">
        {{ productQuery.read.isFetching.value ? 'Refreshing...' : 'Refresh List' }}
      </button>
    </div>
  </div>
</template>

Paginated Queries with Filtering

Use usePaginatedQuery for paginated data with advanced filtering and sorting:

<script setup lang="ts">
  import { Pageable, Filters } from 'gen-query'
  import type { Entity } from 'gen-query'

  interface Product extends Entity<number> {
    name: string
    price: number
    category: string
    stock: number
    createdAt: Date
  }

  const token = ref(localStorage.getItem('token') || undefined)

  // Configure pagination
  const pageable = new Pageable(
    0, // page number (0-indexed)
    20, // page size
    [{ property: 'createdAt', direction: 'desc' }] // sort by newest first
  )

  // Configure filters
  const filters = ref(new Filters())

  // Filter by price range
  filters.value.price = {
    operator: 'and',
    constraints: [
      { matchMode: 'gte', value: 50 }, // price >= 50
      { matchMode: 'lte', value: 500 }, // price <= 500
    ],
  }

  // Filter by category
  filters.value.category = {
    operator: 'and',
    constraints: [{ matchMode: 'eq', value: 'electronics' }],
  }

  // Filter by stock availability
  filters.value.stock = {
    operator: 'and',
    constraints: [{ matchMode: 'gt', value: 0 }], // in stock
  }

  const productQuery = usePaginatedQuery<Product, number>('products', pageable, filters, token)

  // Access paginated data
  const firstPage = computed(() => productQuery.read.data.value?.pages[0])
  const products = computed(() => firstPage.value?.content ?? [])
  const totalPages = computed(() => firstPage.value?.page.totalPages ?? 0)
  const totalElements = computed(() => firstPage.value?.page.totalElements ?? 0)
  const currentPage = computed(() => firstPage.value?.page.number ?? 0)

  // Get all products from all loaded pages (for infinite scroll)
  const allProducts = computed(
    () => productQuery.read.data.value?.pages.flatMap((page) => page.content) ?? []
  )

  // Update filters dynamically
  const updatePriceFilter = (min: number, max: number) => {
    filters.value.price = {
      operator: 'and',
      constraints: [
        { matchMode: 'gte', value: min },
        { matchMode: 'lte', value: max },
      ],
    }
  }

  const updateCategoryFilter = (category: string) => {
    if (category) {
      filters.value.category = {
        operator: 'and',
        constraints: [{ matchMode: 'eq', value: category }],
      }
    } else {
      delete filters.value.category
    }
  }

  const searchByName = (searchTerm: string) => {
    if (searchTerm) {
      filters.value.name = {
        operator: 'and',
        constraints: [{ matchMode: 'contains', value: searchTerm }],
      }
    } else {
      delete filters.value.name
    }
  }
</script>

<template>
  <div>
    <!-- Filters -->
    <div class="filters">
      <input
        type="text"
        placeholder="Search by name..."
        @input="searchByName($event.target.value)"
      />

      <select @change="updateCategoryFilter($event.target.value)">
        <option value="">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="accessories">Accessories</option>
        <option value="computers">Computers</option>
      </select>

      <div>
        Price:
        <input
          type="number"
          placeholder="Min"
          @change="updatePriceFilter($event.target.value, 500)"
        />
        -
        <input
          type="number"
          placeholder="Max"
          @change="updatePriceFilter(50, $event.target.value)"
        />
      </div>
    </div>

    <!-- Loading State -->
    <div v-if="productQuery.read.isLoading.value">Loading products...</div>

    <!-- Error State -->
    <div v-else-if="productQuery.read.isError.value" class="error">
      Error: {{ productQuery.read.error.value?.message }}
    </div>

    <!-- Product List -->
    <div v-else>
      <div class="summary">
        Showing {{ products.length }} of {{ totalElements }} products (Page {{ currentPage + 1 }} of
        {{ totalPages }})
      </div>

      <div v-for="product in products" :key="product.id" class="product-card">
        <h3>{{ product.name }}</h3>
        <p>Price: ${{ product.price }}</p>
        <p>Category: {{ product.category }}</p>
        <p>Stock: {{ product.stock }}</p>
        <p>Added: {{ new Date(product.createdAt).toLocaleDateString() }}</p>
      </div>

      <!-- Pagination Controls -->
      <div class="pagination">
        <button
          @click="productQuery.read.fetchPreviousPage()"
          :disabled="
            !productQuery.read.hasPreviousPage.value ||
            productQuery.read.isFetchingPreviousPage.value
          "
        >
          {{ productQuery.read.isFetchingPreviousPage.value ? 'Loading...' : 'Previous Page' }}
        </button>

        <button
          @click="productQuery.read.fetchNextPage()"
          :disabled="
            !productQuery.read.hasNextPage.value || productQuery.read.isFetchingNextPage.value
          "
        >
          {{ productQuery.read.isFetchingNextPage.value ? 'Loading...' : 'Next Page' }}
        </button>
      </div>
    </div>
  </div>
</template>

Infinite Scroll

For infinite scroll, use all loaded pages:

<script setup lang="ts">
  import { Pageable, Filters } from 'gen-query'
  import type { Entity } from 'gen-query'

  interface Post extends Entity<number> {
    title: string
    content: string
    author: string
  }

  const pageable = new Pageable(0, 10)
  const filters = ref(new Filters())

  const postQuery = usePaginatedQuery<Post, number>('posts', pageable, filters)

  // Get all posts from all loaded pages
  const allPosts = computed(
    () => postQuery.read.data.value?.pages.flatMap((page) => page.content) ?? []
  )

  // Infinite scroll handler
  const handleScroll = () => {
    const scrollPosition = window.innerHeight + window.scrollY
    const threshold = document.documentElement.scrollHeight - 100

    if (
      scrollPosition >= threshold &&
      postQuery.read.hasNextPage.value &&
      !postQuery.read.isFetchingNextPage.value
    ) {
      postQuery.read.fetchNextPage()
    }
  }

  onMounted(() => {
    window.addEventListener('scroll', handleScroll)
  })

  onUnmounted(() => {
    window.removeEventListener('scroll', handleScroll)
  })
</script>

<template>
  <div>
    <div v-if="postQuery.read.isLoading.value">Loading...</div>
    <div v-else>
      <div v-for="post in allPosts" :key="post.id" class="post">
        <h2>{{ post.title }}</h2>
        <p>{{ post.content }}</p>
        <small>By {{ post.author }}</small>
      </div>

      <div v-if="postQuery.read.isFetchingNextPage.value" class="loading">
        Loading more posts...
      </div>

      <div v-else-if="!postQuery.read.hasNextPage.value" class="end">No more posts to load</div>
    </div>
  </div>
</template>

API Reference

Composables

useLoginService(resource?: string)

Creates a login service for authentication.

Parameters:

  • resource (optional): API endpoint for login. Default: 'auth'

Returns:

{
  login: {
    mutate: (credentials: Login, options?) => void
    isPending: Ref<boolean>
    isError: Ref<boolean>
    isSuccess: Ref<boolean>
    error: Ref<ApiError | null>
    data: Ref<User | undefined>
  }
}

useSingleQuery<T, K>(resource: string, id: Ref<K>, token?: MaybeRefOrGetter<string | undefined>)

Fetches and manages a single entity by ID.

Type Parameters:

  • T: Entity type extending Entity<K>
  • K: ID type (e.g., number, string)

Parameters:

  • resource: API endpoint (e.g., 'products')
  • id: Reactive reference to entity ID
  • token (optional): Authentication token (reactive or getter)

Returns:

{
  read: {
    data: Ref<T | undefined>
    error: Ref<ApiError | null>
    isLoading: Ref<boolean>
    isError: Ref<boolean>
    isSuccess: Ref<boolean>
    isFetching: Ref<boolean>
    status: Ref<'loading' | 'error' | 'success'>
    refetch: () => Promise<void>
  },
  create: {
    mutate: (entity: T, options?) => void
    isPending: Ref<boolean>
    isError: Ref<boolean>
    isSuccess: Ref<boolean>
    error: Ref<ApiError | null>
  },
  update: { /* same as create */ },
  del: { /* same as create */ }
}

useMultipleQuery<T, K>(resource: string, token?: MaybeRefOrGetter<string | undefined>)

Provides CRUD operations for a collection of entities.

Type Parameters:

  • T: Entity type extending Entity<K>
  • K: ID type

Parameters:

  • resource: API endpoint
  • token (optional): Authentication token

Returns:

{
  read: {
    data: Ref<T[] | undefined>
    error: Ref<ApiError | null>
    isLoading: Ref<boolean>
    isError: Ref<boolean>
    isSuccess: Ref<boolean>
    isFetching: Ref<boolean>
    refetch: () => Promise<void>
  },
  create: { /* mutation object */ },
  update: { /* mutation object */ },
  del: { /* mutation object */ }
}

usePaginatedQuery<T, K>(resource: string, pageable: Pageable, filters: Ref<Filters>, token?: MaybeRefOrGetter<string | undefined>)

Fetches paginated entities with filtering and sorting.

Type Parameters:

  • T: Entity type extending Entity<K>
  • K: ID type

Parameters:

  • resource: API endpoint
  • pageable: Pagination configuration
  • filters: Reactive filters
  • token (optional): Authentication token

Returns:

{
  read: {
    data: Ref<InfiniteData<Page<T>> | undefined>
    error: Ref<ApiError | null>
    isLoading: Ref<boolean>
    isError: Ref<boolean>
    isSuccess: Ref<boolean>
    isFetching: Ref<boolean>
    isFetchingNextPage: Ref<boolean>
    isFetchingPreviousPage: Ref<boolean>
    hasNextPage: Ref<boolean>
    hasPreviousPage: Ref<boolean>
    fetchNextPage: () => Promise<void>
    fetchPreviousPage: () => Promise<void>
    refetch: () => Promise<void>
  },
  create: { /* mutation object */ },
  update: { /* mutation object */ },
  del: { /* mutation object */ }
}

Types

Entity<K>

Base interface for entities with an ID.

interface Entity<K> {
  id: K
}

Example:

interface Product extends Entity<number> {
  name: string
  price: number
}

Login

Login credentials.

type Login = {
  username: string
  password: string
}

User

User information returned after login.

type User = {
  username: string
  password: string
  fullName: string
  email: string
  token: string
}

Page<T>

Paginated response structure.

type Page<T> = {
  page: {
    number: number // Current page (0-indexed)
    size: number // Items per page
    totalElements: number // Total items across all pages
    totalPages: number // Total number of pages
  }
  content: T[] // Items in current page
}

Sort

Sorting configuration.

type Sort = {
  property: string // Field to sort by
  direction: 'asc' | 'desc' // Sort direction
}

FilterItem

Filter configuration.

type FilterItem = {
  operator: string // 'and' or 'or'
  constraints: Constraint[]
}

Constraint

Filter constraint.

type Constraint = {
  matchMode: string // eq, ne, lt, lte, gt, gte, contains, startsWith, endsWith, in
  value: unknown // Filter value
}

Classes

Pageable

Pagination configuration class.

class Pageable {
  constructor(page: number = 0, size: number = 30, sort: Sort[] = [])

  toQueryParams(): string
}

Example:

const pageable = new Pageable(
  0, // first page
  20, // 20 items per page
  [
    { property: 'name', direction: 'asc' },
    { property: 'price', direction: 'desc' },
  ]
)

Filters

Filter configuration class.

class Filters {
  [key: string]: FilterItem

  toQueryParams(): string
}

Example:

const filters = new Filters()

// Single constraint
filters.category = {
  operator: 'and',
  constraints: [{ matchMode: 'eq', value: 'electronics' }],
}

// Multiple constraints (AND)
filters.price = {
  operator: 'and',
  constraints: [
    { matchMode: 'gte', value: 100 },
    { matchMode: 'lte', value: 500 },
  ],
}

// Multiple constraints (OR)
filters.status = {
  operator: 'or',
  constraints: [
    { matchMode: 'eq', value: 'active' },
    { matchMode: 'eq', value: 'pending' },
  ],
}

// String matching
filters.name = {
  operator: 'and',
  constraints: [{ matchMode: 'contains', value: 'laptop' }],
}

// Date filtering
filters.createdAt = {
  operator: 'and',
  constraints: [{ matchMode: 'gte', value: new Date('2024-01-01') }],
}

Available Match Modes:

| Match Mode | Description | Example | | ------------ | --------------------- | ---------------------------------------------- | | eq | Equals | { matchMode: 'eq', value: 'active' } | | ne | Not equals | { matchMode: 'ne', value: 'deleted' } | | lt | Less than | { matchMode: 'lt', value: 100 } | | lte | Less than or equal | { matchMode: 'lte', value: 100 } | | gt | Greater than | { matchMode: 'gt', value: 50 } | | gte | Greater than or equal | { matchMode: 'gte', value: 50 } | | contains | Contains substring | { matchMode: 'contains', value: 'laptop' } | | startsWith | Starts with | { matchMode: 'startsWith', value: 'Pro' } | | endsWith | Ends with | { matchMode: 'endsWith', value: 'Pro' } | | in | In list | { matchMode: 'in', value: 'active,pending' } |


ApiError

Custom error class for API errors.

class ApiError extends Error {
  timestamp: Date
  type: string
  statusCode: number
  status: string
  content?: object
  stack?: string
}

Example:

const { error } = productQuery.read

if (error.value) {
  console.error('Error:', error.value.message)
  console.error('Status:', error.value.statusCode)
  console.error('Details:', error.value.content)
}

Enums

UpdateStrategy

Cache update strategy for mutations.

enum UpdateStrategy {
  None, // No automatic cache updates
  Invalidate, // Invalidate and refetch (recommended)
  Optimistic, // Optimistic updates
}

Usage:

import { UpdateStrategy } from 'gen-query'

export default defineNuxtConfig({
  genQuery: {
    update: UpdateStrategy.Optimistic,
  },
})

Backend Requirements

Your backend API must follow the REST specification detailed in BACKEND_API.md.

Key requirements:

  • RESTful endpoints with JSON request/response
  • Standard CRUD operations (GET, POST, PUT, DELETE)
  • Pagination endpoint with /page suffix
  • Support for filter and sort query parameters
  • Consistent error response format
  • ISO 8601 date format
  • Optional Bearer token authentication

Development

# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with playground
npm run dev

# Build playground
npm run dev:build

# Run ESLint
npm run lint

# Run type checking
npm run test:types

# Run tests
npm run test
npm run test:watch

# Release new version
npm run release

License

MIT License - see LICENSE for details

Links