Skip to main content
Strapi relations connect content types together. strapi2front generates fully-typed interfaces and helpers for working with all relation types.

Relation Types

Strapi supports four relation types, all handled automatically by strapi2front:

One-to-One

Single item relates to single item (e.g., Article → Author)

One-to-Many

Single item relates to multiple items (e.g., Author → Articles)

Many-to-One

Multiple items relate to single item (e.g., Articles → Category)

Many-to-Many

Multiple items relate to multiple items (e.g., Articles ↔ Tags)

Generated Types for Relations

To-One Relations

For one-to-one and many-to-one relations, the generated type is nullable:
article.ts
import type { Author } from './author';
import type { Category } from './category';

export interface Article extends StrapiBaseEntity {
  title: string;
  
  // Many-to-one: many articles can have the same author
  author: Author | null;
  
  // Many-to-one: many articles can be in the same category
  primaryCategory: Category | null;
}

To-Many Relations

For one-to-many and many-to-many relations, the generated type is an array:
article.ts
import type { Category } from './category';
import type { Tag } from './tag';

export interface Article extends StrapiBaseEntity {
  title: string;
  
  // Many-to-many: articles can have multiple categories
  categories: Category[];
  
  // Many-to-many: articles can have multiple tags
  tags: Tag[];
}

Populating Relations

By default, Strapi returns only relation IDs. Use populate to fetch the full related data.

Basic Population

// Populate all first-level relations
const article = await articleService.findOne('abc123', {
  populate: '*',
});

// Now you can access full relation data
console.log(article.author?.name);
console.log(article.categories.map(c => c.name));
Using populate: '*' can be slow and return large responses. Use selective population in production.

Population in Queries

// Populate when fetching multiple items
const { data: articles } = await articleService.findMany({
  filters: { status: 'published' },
  populate: ['author', 'categories', 'coverImage'],
  sort: '-publishedAt',
});

articles.forEach(article => {
  console.log(`${article.title} by ${article.author?.name}`);
});

Creating Relations

When creating or updating content, use documentId (v5) or id (v4) to link relations.

Simple Relation Format

import { articleService } from '@/strapi/collections/article/service';

const newArticle = await articleService.create({
  title: 'Getting Started with TypeScript',
  slug: 'getting-started-typescript',
  content: [],
  
  // To-one: use documentId string
  author: 'author-doc-id-123',
  
  // To-many: use array of documentIds
  categories: ['cat-doc-id-1', 'cat-doc-id-2'],
  tags: ['tag-doc-id-a', 'tag-doc-id-b'],
});

Advanced Relation Format (Strapi v5)

Strapi v5 supports advanced relation operations with connect, disconnect, and set:
1

connect

Add relations while preserving existing ones
await articleService.update('abc123', {
  categories: {
    connect: ['new-category-id'],
  },
});
2

disconnect

Remove specific relations without affecting others
await articleService.update('abc123', {
  tags: {
    disconnect: ['tag-to-remove-id'],
  },
});
3

set

Replace ALL relations with a new set
await articleService.update('abc123', {
  categories: {
    set: ['cat-1', 'cat-2', 'cat-3'],
  },
});
// Add and remove relations in one operation
await articleService.update('abc123', {
  categories: {
    // Add new categories
    connect: [
      { documentId: 'new-cat-1' },
      { documentId: 'new-cat-2', position: { start: true } },
    ],
    // Remove old categories
    disconnect: [
      { documentId: 'old-cat-1' },
    ],
  },
});

// With i18n and draft/publish
await articleService.update('abc123', {
  relatedArticles: {
    connect: [
      { 
        documentId: 'related-1',
        locale: 'en',
        status: 'published',
      },
      {
        documentId: 'related-2',
        locale: 'fr',
        status: 'draft',
      },
    ],
  },
});

// Control ordering with position
await articleService.update('abc123', {
  sections: {
    connect: [
      { documentId: 'section-1', position: { start: true } },
      { documentId: 'section-2', position: { after: 'section-1' } },
      { documentId: 'section-3', position: { end: true } },
    ],
  },
});

Querying by Relations

Filter content by related items:
// Find articles by a specific author
const { data } = await articleService.findMany({
  filters: {
    author: { documentId: { $eq: 'author-123' } },
  },
});

Real-World Examples

Blog with Author and Categories

pages/blog/[slug].astro
---
import { articleService } from '@/strapi/collections/article/service';
import type { Article } from '@/strapi/collections/article/types';

const { slug } = Astro.params;

const article = await articleService.findBySlug(slug, {
  populate: {
    author: {
      populate: ['avatar'],
    },
    categories: true,
    tags: true,
    coverImage: true,
  },
  status: 'published',
});

if (!article) {
  return Astro.redirect('/404');
}

// Get related articles by same author
const { data: relatedByAuthor } = await articleService.findMany({
  filters: {
    author: { documentId: { $eq: article.author?.documentId } },
    documentId: { $ne: article.documentId },  // Exclude current
    status: 'published',
  },
  pagination: { pageSize: 3 },
  populate: ['coverImage'],
});

// Get related articles in same categories
const categoryIds = article.categories.map(c => c.documentId);
const { data: relatedByCategory } = await articleService.findMany({
  filters: {
    categories: {
      documentId: { $in: categoryIds },
    },
    documentId: { $ne: article.documentId },
    status: 'published',
  },
  pagination: { pageSize: 3 },
  populate: ['coverImage', 'author'],
});
---

<article>
  <h1>{article.title}</h1>
  
  {article.author && (
    <div class="author">
      {article.author.avatar && (
        <img src={article.author.avatar.url} alt={article.author.name} />
      )}
      <span>By {article.author.name}</span>
    </div>
  )}
  
  <div class="categories">
    {article.categories.map(category => (
      <a href={`/category/${category.slug}`}>{category.name}</a>
    ))}
  </div>
  
  <div class="tags">
    {article.tags.map(tag => (
      <span class="tag">{tag.name}</span>
    ))}
  </div>
  
  <div class="content">
    {/* Article content */}
  </div>
  
  {relatedByAuthor.length > 0 && (
    <section>
      <h2>More from {article.author?.name}</h2>
      <div class="grid">
        {relatedByAuthor.map(related => (
          <ArticleCard article={related} />
        ))}
      </div>
    </section>
  )}
  
  {relatedByCategory.length > 0 && (
    <section>
      <h2>Related Articles</h2>
      <div class="grid">
        {relatedByCategory.map(related => (
          <ArticleCard article={related} />
        ))}
      </div>
    </section>
  )}
</article>

Filter Articles by Category

pages/category/[slug].astro
---
import { categoryService } from '@/strapi/collections/category/service';
import { articleService } from '@/strapi/collections/article/service';

const { slug } = Astro.params;
const page = Number(Astro.url.searchParams.get('page')) || 1;

const category = await categoryService.findBySlug(slug);

if (!category) {
  return Astro.redirect('/404');
}

const { data: articles, pagination } = await articleService.findMany({
  filters: {
    categories: {
      documentId: { $eq: category.documentId },
    },
    status: 'published',
  },
  sort: '-publishedAt',
  pagination: { page, pageSize: 12 },
  populate: ['author', 'coverImage'],
});
---

<div>
  <h1>{category.name}</h1>
  {category.description && <p>{category.description}</p>}
  
  <div class="articles">
    {articles.map(article => (
      <ArticleCard article={article} />
    ))}
  </div>
  
  <Pagination 
    currentPage={pagination.page}
    totalPages={pagination.pageCount}
  />
</div>

Author Profile with Articles

pages/author/[slug].astro
---
import { authorService } from '@/strapi/collections/author/service';
import { articleService } from '@/strapi/collections/article/service';

const { slug } = Astro.params;

const author = await authorService.findBySlug(slug, {
  populate: ['avatar', 'socialLinks'],
});

if (!author) {
  return Astro.redirect('/404');
}

// Get all articles by this author
const { data: articles, pagination } = await articleService.findMany({
  filters: {
    author: {
      documentId: { $eq: author.documentId },
    },
    status: 'published',
  },
  sort: '-publishedAt',
  populate: ['coverImage', 'categories'],
});

// Get article count
const articleCount = await articleService.count({
  author: { documentId: { $eq: author.documentId } },
  status: 'published',
});
---

<div class="author-profile">
  <div class="author-header">
    {author.avatar && (
      <img src={author.avatar.url} alt={author.name} class="avatar" />
    )}
    <h1>{author.name}</h1>
    {author.bio && <p>{author.bio}</p>}
    <p>{articleCount} articles published</p>
  </div>
  
  <div class="articles">
    <h2>Articles by {author.name}</h2>
    {articles.map(article => (
      <ArticleCard article={article} />
    ))}
  </div>
</div>

Bidirectional Relations

When relations are bidirectional (defined on both sides), Strapi keeps them in sync:
// Article has many Categories
// Category has many Articles

// Adding a category to an article...
await articleService.update('article-123', {
  categories: {
    connect: ['category-456'],
  },
});

// ...automatically updates the category's articles
const category = await categoryService.findOne('category-456', {
  populate: ['articles'],
});
// category.articles now includes article-123
Bidirectional relations are updated automatically by Strapi. You only need to update one side.

Performance Tips

// Bad: Over-fetching
const article = await articleService.findOne('abc123', {
  populate: '*',
});

// Good: Selective population
const article = await articleService.findOne('abc123', {
  populate: ['author', 'coverImage'],
});
// Can be slow with large datasets
const article = await articleService.findOne('abc123', {
  populate: {
    author: {
      populate: {
        articles: {
          populate: ['categories', 'tags'],
        },
      },
    },
  },
});

// Better: Fetch separately if needed
const article = await articleService.findOne('abc123', {
  populate: ['author'],
});

if (article.author) {
  const { data: authorArticles } = await articleService.findMany({
    filters: { author: { documentId: { $eq: article.author.documentId } } },
    pagination: { pageSize: 5 },
  });
}
If a content type has hundreds of relations, consider paginating:
// Instead of populating all articles
const category = await categoryService.findOne('cat-123', {
  populate: ['articles'],  // Could be 1000+ items
});

// Fetch articles separately with pagination
const category = await categoryService.findOne('cat-123');
const { data: articles } = await articleService.findMany({
  filters: { categories: { documentId: { $eq: 'cat-123' } } },
  pagination: { page: 1, pageSize: 20 },
});

Next Steps