Files
shiftcraft/.claude/skills/pocketbase-best-practices/rules/query-first-item.md
2026-04-17 23:26:01 +00:00

4.2 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use getFirstListItem for Single Record Lookups MEDIUM Cleaner code, automatic error handling for not found query, lookup, find, getFirstListItem

Use getFirstListItem for Single Record Lookups

Use getFirstListItem() when you need to find a single record by a field value other than ID. It's cleaner than getList() with limit 1 and provides proper error handling.

Incorrect (manual single-record lookup):

// Using getList with limit 1 - verbose
async function findUserByEmail(email) {
  const result = await pb.collection('users').getList(1, 1, {
    filter: pb.filter('email = {:email}', { email })
  });

  if (result.items.length === 0) {
    throw new Error('User not found');
  }

  return result.items[0];
}

// Using getFullList then filtering - wasteful
async function findUserByUsername(username) {
  const users = await pb.collection('users').getFullList({
    filter: pb.filter('username = {:username}', { username })
  });
  return users[0];  // Might be undefined!
}

// Fetching by ID when you have a different identifier
async function findProductBySku(sku) {
  // Wrong: getOne expects the record ID
  const product = await pb.collection('products').getOne(sku);  // Fails!
}

Correct (using getFirstListItem):

// Clean single-record lookup by any field
async function findUserByEmail(email) {
  try {
    const user = await pb.collection('users').getFirstListItem(
      pb.filter('email = {:email}', { email })
    );
    return user;
  } catch (error) {
    if (error.status === 404) {
      return null;  // Not found
    }
    throw error;
  }
}

// Lookup by unique field
async function findProductBySku(sku) {
  return pb.collection('products').getFirstListItem(
    pb.filter('sku = {:sku}', { sku })
  );
}

// Lookup with expand
async function findOrderByNumber(orderNumber) {
  return pb.collection('orders').getFirstListItem(
    pb.filter('orderNumber = {:num}', { num: orderNumber }),
    { expand: 'customer,items' }
  );
}

// Complex filter conditions
async function findActiveSubscription(userId) {
  return pb.collection('subscriptions').getFirstListItem(
    pb.filter(
      'user = {:userId} && status = "active" && expiresAt > @now',
      { userId }
    )
  );
}

// With field selection
async function getUserIdByEmail(email) {
  const user = await pb.collection('users').getFirstListItem(
    pb.filter('email = {:email}', { email }),
    { fields: 'id' }
  );
  return user.id;
}

Comparison with getOne:

// getOne - fetch by record ID
const post = await pb.collection('posts').getOne('abc123');

// getFirstListItem - fetch by any filter (use pb.filter for safe binding)
const post = await pb.collection('posts').getFirstListItem(
  pb.filter('slug = {:slug}', { slug: 'hello-world' })
);
const user = await pb.collection('users').getFirstListItem(
  pb.filter('username = {:name}', { name: 'john' })
);
const order = await pb.collection('orders').getFirstListItem(
  pb.filter('orderNumber = {:num}', { num: 12345 })
);

Error handling:

// getFirstListItem throws 404 if no match found
try {
  const user = await pb.collection('users').getFirstListItem(
    pb.filter('email = {:email}', { email })
  );
  return user;
} catch (error) {
  if (error.status === 404) {
    // No matching record - handle appropriately
    return null;
  }
  // Other error (network, auth, etc.)
  throw error;
}

// Wrapper function for optional lookup
async function findFirst(collection, filter, options = {}) {
  try {
    return await pb.collection(collection).getFirstListItem(filter, options);
  } catch (error) {
    if (error.status === 404) return null;
    throw error;
  }
}

// Usage
const user = await findFirst('users', pb.filter('email = {:e}', { e: email }));
if (!user) {
  console.log('User not found');
}

When to use each method:

Method Use When
getOne(id) You have the record ID
getFirstListItem(filter) Finding by unique field (email, slug, sku)
getList(1, 1, { filter }) Need pagination metadata
getFullList({ filter }) Expecting multiple results

Reference: PocketBase Records API