120k

Dynamic Search

Implement server-side search for large registries.

By default, shadcn search fetches your entire registry.json and filters items locally. This works well for most registries and requires nothing more than a static file.

For large registries with thousands of items, you can implement search on your registry server instead. The CLI forwards the search parameters to your registry and your server returns only the matching items.

Dynamic search is opt-in and fully backwards compatible. Static registries keep working without any changes.

How It Works

When you run shadcn search, the CLI appends the search parameters to the catalog request:

pnpm dlx shadcn@latest search @acme --query button --limit 50
GET https://acme.com/r/registry.json?q=button&limit=50&offset=0

What happens next depends on your registry:

  • Static registries ignore the query parameters and return the full registry.json. The CLI filters the items locally. This is the default behavior and requires no changes.
  • Dynamic registries filter the items server-side and return the matching items along with a pagination object. When the CLI sees pagination in the response, it trusts the results as pre-filtered and skips local filtering.

The presence of pagination in the response is what tells the CLI your registry handles search server-side. There is no configuration or capability negotiation required.

Query Parameters

The CLI sends the following query parameters with every search request:

ParameterDescription
qThe search query string.
typeComma-separated item types, e.g. registry:ui,registry:block.
limitMaximum number of items to return.
offsetNumber of items to skip.

All parameters are optional. A request without q or type should return all items, paginated.

Response Format

Return your regular registry.json shape with an additional pagination object:

registry.json?q=button&limit=2
{
  "name": "acme",
  "homepage": "https://acme.com",
  "items": [
    {
      "name": "button",
      "type": "registry:ui",
      "description": "A button component."
    },
    {
      "name": "icon-button",
      "type": "registry:ui",
      "description": "A button component with an icon."
    }
  ],
  "pagination": {
    "total": 12,
    "offset": 0,
    "limit": 2,
    "hasMore": true
  }
}

Search results only need name, type and description for each item. You do not need to include files, dependencies or other item properties. The CLI fetches the full item definition when the user runs shadcn add.

pagination

PropertyTypeDescription
totalnumberTotal number of items matching the query.
offsetnumberNumber of items skipped.
limitnumberMaximum number of items in this response.
hasMorebooleanWhether more items are available beyond this page.

Server Implementation

Here's an example using a Next.js route handler:

app/r/registry.json/route.ts
import { NextRequest, NextResponse } from "next/server"
 
export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl
 
  const query = searchParams.get("q")
  const types = searchParams.get("type")?.split(",")
  const limit = Number(searchParams.get("limit") ?? 100)
  const offset = Number(searchParams.get("offset") ?? 0)
 
  // Filter items using your database or search index.
  const { items, total } = await searchItems({ query, types, limit, offset })
 
  return NextResponse.json({
    name: "acme",
    homepage: "https://acme.com",
    items,
    pagination: {
      total,
      offset,
      limit,
      hasMore: offset + limit < total,
    },
  })
}

You can back searchItems with anything: a database query, a full-text search index or an external search service.

Multiple Registries

When searching a single registry, the CLI forwards all parameters and uses your pagination response as-is.

When searching multiple registries at once, e.g. shadcn search @acme @lib, a global offset cannot be split across registries. The CLI forwards q and type to each registry along with a limit large enough to fill the requested page (offset + limit), then merges and paginates the combined results locally. Server-side filtering still applies, so each registry only returns matching items.

If your registry caps the number of items per response below the requested limit, the CLI treats it as exhausted for deeper pages. Honor the requested limit where possible so all your matches stay reachable through paging.

Authentication

Dynamic search works with all authentication patterns. The CLI sends the configured headers and params with the search request, so you can scope search results to the authenticated user:

app/r/registry.json/route.ts
export async function GET(request: NextRequest) {
  const token = request.headers.get("authorization")?.replace("Bearer ", "")
  const team = await getTeamFromToken(token)
 
  // Only search items this team can access.
  const { items, total } = await searchItems({
    query: request.nextUrl.searchParams.get("q"),
    team,
  })
 
  // ...
}

Backwards Compatibility

  • Static registries require no changes. Query parameters on a static file are ignored by the file server and the CLI falls back to local filtering.
  • Older CLI versions fetch the catalog without query parameters and ignore the pagination field. Your registry should return a sensible default response for requests without parameters, e.g. the first page of items.
  • Ranking is up to your server. When your registry returns pre-filtered results, the CLI preserves your order instead of re-ranking locally.

Testing

Test your dynamic registry with curl:

curl "https://acme.com/r/registry.json?q=button&limit=10"

Then verify with the CLI:

pnpm dlx shadcn@latest search @acme --query button

To confirm server-side search is active, check that the response includes the pagination object and only the matching items.