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

3.9 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Expand Relations Efficiently HIGH Eliminates N+1 queries, reduces API calls by 90%+ query, relations, expand, joins, performance

Expand Relations Efficiently

Use the expand parameter to fetch related records in a single request. This eliminates N+1 query problems and dramatically reduces API calls.

Incorrect (N+1 queries):

// Fetching posts then authors separately - N+1 problem
async function getPostsWithAuthors() {
  const posts = await pb.collection('posts').getList(1, 20);

  // N additional requests for N posts!
  for (const post of posts.items) {
    post.authorData = await pb.collection('users').getOne(post.author);
  }

  return posts;
}
// 21 API calls for 20 posts!

// Even worse with multiple relations
async function getPostsWithAll() {
  const posts = await pb.collection('posts').getList(1, 20);

  for (const post of posts.items) {
    post.author = await pb.collection('users').getOne(post.author);
    post.category = await pb.collection('categories').getOne(post.category);
    post.tags = await Promise.all(
      post.tags.map(id => pb.collection('tags').getOne(id))
    );
  }
  // 60+ API calls!
}

Correct (using expand):

// Single request with expanded relations
async function getPostsWithAuthors() {
  const posts = await pb.collection('posts').getList(1, 20, {
    expand: 'author'
  });

  // Access expanded data
  posts.items.forEach(post => {
    console.log('Author:', post.expand?.author?.name);
  });

  return posts;
}
// 1 API call!

// Multiple relations
async function getPostsWithAll() {
  const posts = await pb.collection('posts').getList(1, 20, {
    expand: 'author,category,tags'
  });

  posts.items.forEach(post => {
    console.log('Author:', post.expand?.author?.name);
    console.log('Category:', post.expand?.category?.name);
    console.log('Tags:', post.expand?.tags?.map(t => t.name));
  });
}
// Still just 1 API call!

// Nested expansion (up to 6 levels)
async function getPostsWithNestedData() {
  const posts = await pb.collection('posts').getList(1, 20, {
    expand: 'author.profile,category.parent,comments_via_post.author'
  });

  posts.items.forEach(post => {
    // Nested relations
    console.log('Author profile:', post.expand?.author?.expand?.profile);
    console.log('Parent category:', post.expand?.category?.expand?.parent);

    // Back-relations (comments that reference this post)
    console.log('Comments:', post.expand?.['comments_via_post']);
  });
}

// Back-relation expansion
// If comments collection has a 'post' relation field pointing to posts
async function getPostWithComments(postId) {
  const post = await pb.collection('posts').getOne(postId, {
    expand: 'comments_via_post,comments_via_post.author'
  });

  // Access comments that reference this post
  const comments = post.expand?.['comments_via_post'] || [];
  comments.forEach(comment => {
    console.log(`${comment.expand?.author?.name}: ${comment.text}`);
  });

  return post;
}

Expand syntax:

Syntax Description
expand: 'author' Single relation
expand: 'author,tags' Multiple relations
expand: 'author.profile' Nested relation (2 levels)
expand: 'comments_via_post' Back-relation (records pointing to this)

Handling optional expand data:

// Always use optional chaining - expand may be undefined
const authorName = post.expand?.author?.name || 'Unknown';

// Type-safe access with TypeScript
interface Post {
  id: string;
  title: string;
  author: string;  // Relation ID
  expand?: {
    author?: User;
  };
}

const posts = await pb.collection('posts').getList<Post>(1, 20, {
  expand: 'author'
});

Limitations:

  • Maximum 6 levels of nesting
  • Respects API rules on expanded collections
  • Large expansions may impact performance

Reference: PocketBase Expand