Writing
February 6, 2026 · 9 min read

Building a Unified Library Search: A Journey from Frustration to Solution

Back to itch my scratch and build for n=1

The Problem

I’m part of the Whitchurch-Stouffville community, and like many residents, I rely on our local public library. But there’s a challenge: Stouffville has the smallest collection among York Region’s 9 public libraries—only about ~35,000 books compared to Markham’s 278,000 or for that matter Aurora/Newmarket/Georgina of comparable population. Population stats here.

When I can’t find what I need locally, I switch to searching Markham’s catalog, since we have a subscription with them too. Each search means:

  1. Opening a new browser tab
  2. Typing the same query again
  3. Comparing results manually
  4. Remembering which library had what

It’s tedious. There had to be a better way.

The Discovery

All York Region libraries use BiblioCommons, an excellent platform that powers library catalogs for over 300 public libraries worldwide. Each library has its own BiblioCommons site:

BiblioCommons offers fantastic features on each site: staff curated lists, author picks, community reviews, patron ratings, reading recommendations, event calendars, and more. It’s genuinely well-designed software.

But there was no way to search across multiple libraries at once.

The Question

I asked Claude (Anthropic’s AI assistant) via Kilo:

“Is there a way I can execute a search across all the libraries to find what I need? Are there any open source applications that does this or should I build it?”

Research Phase

Claude researched existing solutions:

What Exists:

Build Options Considered:

  1. Bookmarklet: One-click opens 4 tabs (quickest)
  2. 📄 Local HTML page: Search form opening multiple tabs (simple)
  3. 🚀 Web/Mobile App: Aggregates results into one view (best UX)

The Breakthrough: Finding the Internal API

Here’s where it got interesting. I discovered that BiblioCommons uses an internal JSON API (they do not have a public-facing API documentation - that I could find):

POST https://gateway.bibliocommons.com/v2/libraries/{subdomain}/bibs/search
Body: {"query":"search term","searchType":"smart","view":"grouped"}

This wasn’t publicly documented, but it’s what powers their web interfaces. And it returns beautiful, structured JSON with everything we need:

Mapping All 9 Libraries

With the API endpoint discovered, we mapped all York Region libraries:

Building the App

With AI as my pair programmer, we built a cross-platform solution:

The Architecture

1. User enters search query
2. App queries all 9 libraries in parallel
3. Results aggregated and deduplicated
4. Displayed with availability status
5. User can filter by library, format, or availability

Simple. Fast. Effective.

Linux App

Mobile demo

The Stats Challenge: No Direct API

The PM in me introduced a scope creep - having solved the primary need - search across libraries, I turned to data. I wanted to know how many items are there in each and across all the libraries? Also, read the stats disclaimer about extrapolation of this data.

Here’s where it got technically interesting. I wanted to show collection statistics—how many books each library has, broken down by format. But BiblioCommons doesn’t have an endpoint for “total items in catalog.”

The Journey to Accurate Counts

Attempt 1: Query 'a' with searchType: 'keyword'

Attempt 2: Query '*' with searchType: 'keyword'

Attempt 3: Query '*' with searchType: 'smart'

Final Solution: Query 'anywhere:(a the)' with searchType: 'bl'

The Parsing Bug

Then we hit another bug. The logs showed:

Fields Available: ['0', '1', '2', '3', '4', ...]
NO FORMAT FIELD FOUND!

Wait... fields is an array? I assumed it was an object!

Looking at actual API responses revealed:

"fields": [
  {"id": "FORMAT", "fieldFilters": [{"value": "BK", "count": 12345}, ...]},
  {"id": "AUTHOR", "fieldFilters": [...]},
  {"id": "STATUS", "fieldFilters": [...]}
]

The code was trying fields.FORMAT (object access) when it should have been finding the FORMAT element in the array. Fixed in both platforms by:

const formatField = fields.find(f => f.id === 'FORMAT');

What You Get

Grouped Format Display

Instead of showing raw format codes (BK, LPRINT, PICTURE_BOOK, GRAPHIC_NOVEL...), the app groups them logically:

---Made up numbers below---

Much cleaner than 16 individual format codes!

Opt-In Stats

Based on feedback, stats don’t autoload. Instead, you see a button: “View Collection Statistics”

Click it to see what’s available across all 9 libraries. This was important because:

  1. Stats aren’t the main purpose—search is
  2. The API call is somewhat expensive (queries all 9 libraries)
  3. Users searching for specific titles don’t need stats
  4. Stats are “scope creep”—nice to have, not essential

Acknowledging BiblioCommons

It’s worth emphasizing: BiblioCommons is excellent software. Their platform provides:

Each library’s individual BiblioCommons site is feature-rich and well-designed.

My app doesn’t replace any of that. It complements it by solving one specific problem: searching across multiple libraries when you have multiple memberships.

The Stats Disclaimer

About those collection numbers: they’re approximations. BiblioCommons doesn’t have a “give me your total item count” API endpoint.

Our method:

Query: anywhere:(a the)
Search Type: bl (broad library)

This broad query captures a comprehensive portion of each catalog while returning format breakdowns in the response. The counts aren’t perfect, but they’re representative and give you a sense of relative collection sizes.

But here’s the thing: the stats don’t matter much. That’s not why I built this. The core purpose is unified search. The statistics feature was scope creep—something I added because “why not?” and “it would be interesting to see.”

Hold that thought! What if stats matter to you? Because municipal elections are around the corner - how does this stats tie into the population, and in-turn, budget allocation and use, and there by influencing where your vote lies!

For searching specific titles across libraries? The stats are irrelevant. You just need the search results.

What’s Next?

Right now, this is a personal project running locally for my household. Both my partner and I use it regularly—it saves time and frustration.

But if there’s community interest, I could:

Try It Yourself

The app is available in three flavors:

All three stay in sync, sharing the same core logic and search algorithms.


Technical Deep Dive: The Stats Methodology

For fellow developers curious about the implementation:

The Problem

No direct API endpoint for “total items” or “format breakdown by count.”

The Solution

Use the search API creatively:

Query Crafting:

{
  "query": "anywhere:(a the)",
  "searchType": "bl",
  "limit": 1
}

Response Structure:

{
  "catalogSearch": {
    "pagination": {
      "count": 278479  // ← Total matching items
    },
    "fields": [  // ← Array, not object!
      {
        "id": "FORMAT",
        "fieldFilters": [
          {"value": "BK", "count": 123456},
          {"value": "EBOOK", "count": 45678},
          {"value": "DVD", "count": 12345},
          ...
        ]
      }
    ]
  }
}

Key Discovery: fields is an array of field objects, not an object with named keys. This caused hours of debugging until we looked at actual API responses!

Grouping Logic:

// Combine related formats
Books = BK + LPRINT + PICTURE_BOOK + GRAPHIC_NOVEL + BOARD_BK + BOOK_CD
Audiobooks = AB + EAUDIOBOOK
Movies = DVD + BLURAY + VIDEO_ONLINE
// etc.

Caching:

Result: Approximate but representative collection statistics with format breakdowns, all without an official stats API.

Lessons Learned

  1. Undocumented APIs can be gold: BiblioCommons’ internal API is clean and well-structured
  2. Assumptions are dangerous: “Fields will be an object” cost us hours
  3. Logs are essential: Server-side logging revealed the array structure
  4. AI as pair programmer works: Claude helped architecture, debugging, and implementation
  5. Scope creep is real: Stats were “nice to have” that became a rabbit hole
  6. Test with real data: The difference between 'a', '*', and 'anywhere:(a the)' was huge

The Value Proposition

If you’re a York Region resident with memberships at Stouffville and Markham (or any combination), this app saves you:

For Stouffville residents especially, it opens up access to 1.5+ million items across York Region, not just our local 35,000.


Final Thought: Sometimes the best solutions come from personal frustration. I built this because I needed it. If it helps you too, that’s wonderful. If not, at least I learned a lot about library APIs, cross-platform development, and creative data extraction!

Built with Flutter, React, TypeScript, and Claude AI assistance. Not affiliated with York Region Libraries or BiblioCommons.

← All writing Home