✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

APIs41 min read

Understanding and Using Relations in Strapi

May 16, 2022Updated on June 14, 2026
Understanding and Using Relations in Strapi

In this article, we will look into the relational fields in Strapi to see how we can utilize them to establish relationships in our models.

What is Strapi?

Strapi is an open-source Node.js headless CMS(Content Management System) based on Node.js used to develop APIs(RESTful and GraphQL APIs) and build the APIs content. The APIs in Strapi are built in the form of collections or single types.

A collection in Strapi will create and expose the endpoints on all the HTTP verbs. For example, if we have a blog collection. Strapi will create the following endpoints based on the collection:

  • blog GET: This will get all the blog entries from the endpoint.
  • blog POST: This will create a new blog post from the endpoint.
  • blog/:documentId GET: This will return the blog post with the document ID :documentId.
  • blog/:documentId DELETE: This will delete the blog post with the document ID :documentId from the endpoint.

Strapi creates all those APIs for us. We can then add content to the collection via the admin panel or the Strapi API.

Internally, Strapi is powered by Koajs, and its default database is SQLite, where it persists the content we add to the collections and single-types.

Now, you will learn about relations in database models and establish the relations in Strapi collections.

Relations in Database Fields and Strapi

The database contains tables, columns, and records. Now, relationships can be defined in the database tables. In Strapi, we can use relations to create links between our Content Types. This relationship is like a pointer or reference. They point to data in a table that depicts what they contain.

There are types of relationships we can establish in Strapi:

  • One-to-one (1:1)
  • One-to-Many
  • Many-to-Many
  • One-Way
  • Many-way
  • Polymorphic

Strapi One-to-one relationship (1:1)

In this Strapi one-to-one relationship, a column in a table points to only one column in another table.

For example, in a Student table, a studentId column can point to a StudentInfo table. A column in the StudentInfo table, studentId points back to the Student table. So here, the Student table is associated with one and only one record in the StudentInfo table. We can fetch a student's info from the Student table, and we can fetch a student from the StudentInfo table. That's a one-to-one relationship.

Strapi One-to-Many Relationship

This relation involves a table pointing to several or many tables. A column in table A can point to several tables(B, C, D), these tables, in turn, point to table A. Also, each table (A, B, C, D) can hold one or more records of the column in table A.

For example, let's say we have a Company table. This table holds the list of all the companies in a system. We can create an Employee table to hold the name of an employee. Now, we can add a companyId column to the Employee table, and this companyId will point to the Company table.

Now a Company table can point to many employee records in the Employee table. Also, each record in the Employee table points back to a record in the Company table. The relation here is Strapi one-to-many relationship.

Strapi Many-to-Many Relationship

Strapi many-to-many relationship involves a column in a table pointing to many records in another table and a column in another table pointing to many records in the first table. For example, many doctors can be associated with many hospitals.

Strapi One-Way Relationship

This relationship involves a column pointing or linking to another column in a table. The thing here is that the other column does not point back to the "pointing" column. One-way relation is similar to One-to-One relation but differs because the column being "pointed" does not link back to the pointing column.

For example, in a User table, A detailsId column in the User table can point to a Details table. This means that the details of a user are in the detailsId column in the User table and the details are stored in the Details table.

So we see that the User table points to only one table, which is the Details table. The relationship is one-way. There is no column in the Details table that points back to the User table.

Strapi Many-way Relationship

This relation involves a column in a table pointing to many records in another table. The records being pointed to does not point back or link back to the record.

For example, a User table has a column carId that points to a Car table. The carId can point to many records in the Car table but the Car record does not point back to the User table, this relationship is a Strapi many-way relationship.

Polymorphic

This relationship involves a column in a table that can link to different columns in other tables. In a polymorphic relationship, a model/table can be associated with different models/tables. In other relationships we have seen, it is mainly between a table and another table, not more than three tables are involved in the relationship. But in a polymorphic relationship, multiple tables are involved.

For example, a Tire table holds can be linked and have links to a Toyota table, Mercedes table, etc. So a Toyota can relate to the same Tire as a Mercedes. We have seen all the relations we have. The below sections will explain and show how we can set the relations from both the Strapi admin UI and a Strapi project.

Where are relations set in Strapi?

Relationship links can be set in the Admin panel and manually from the generated Strapi project.

Via Strapi Admin Panel

Relations can be set in Strapi's Collection types, Single types, and Components. The relation is set when adding fields to our Collection, Single collection, or Component type. The relation field is selected:

Content Type builder modal showing field types - Content-Type Builder _ Strapi · 2.32pm · 09-06

Another UI is displayed in the modal:

User relation field modal - Content-Type Builder _ Strapi · 2.36pm · 09-06

This is where we set the relations between the current model we are creating and an existing model.

We have two big boxes in the above picture, the left box is the current model we are creating, and the right box is the model the current model will be having relations with. We can click on the dropdown icon to select the model we want to link relations within the right box.

Content-Type Builder _ Strapi · 2.39pm · 09-06

The smaller boxes with icons are the relations we can establish between the two models in the bigger boxes.

Let's look at the smaller boxes starting from the left.

  • The first box represents the has one relation.

one-way relation

It establishes a one-way relation between content types in Strapi.

  • The second box is has one and belongs to one.

one-to-one relation

It links two content types in a one-to-one way relationship.

  • The third box is belongs to many.

one-to-many relation

It links two content types in a one-to-many relation. The content type in the left-bigger box will have a field that links to many records in the content type that is in the right-bigger box. The field in the content type in the right-bigger box will have a field that links to a single record in the left-content type.

  • The fourth box is has many.

many-to-one relation

This one links two content types in a many-to-one relation. Here, the content type at the left-bigger box has a field that links to many records to the content type at the right-bigger box. It is the reverse of the belongs to many boxes.

  • The fifth box is has and belongs to many.

many-to-many relation

This box links two content types in a many-to-many relationship. Both content types in the bigger boxes will have a field that links many records to each other.

  • The sixth box is has many.

many-way relation

It links two content types in a many-way relationship. The field on the left content type links to many records in the right content type. The right content type does not link back to the left content type.

Via Strapi Project

Let's see how we set relations in our content types from our Strapi project. The content types in a Strapi project are stored in the ./src/api/ folder in our Strapi project. The relations are set in the ./src/api/[NAME]/content-types/[NAME]/schema.json file.

Fields are set inside the attributes section. To set a relation field we use some properties like model, collection, etc. Let's see how we set the relations for all the types of relations in Strapi.

One-to-One (1:1)

To set a one-to-one relation between two content types, we’ll create a new property in the attributes property. Let's say we want to set a one-to-one between a Student model and a Student-info model, we will open the ./src/api/student/content-types/student/schema.json file and add the code:

    {
      "kind": "collectionType",
      "collectionName": "students",
      "info": {
        "singularName": "student",
        "pluralName": "students",
        "displayName": "Student",
        "description": ""
      },
      "options": {
        "draftAndPublish": true
      },
      "pluginOptions": {},
    
      // The fields are configured here
      "attributes": {
        "name": {
          "type": "string"
        },
    
        "student_info": { //field name
          "type": "relation", // field type
          "relation": "oneToOne", // relation type
          "target": "api::student-info.student-info", // the target of the relation
          "inversedBy": "student" // more info here - https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#relations
        }
      }
    }

The relation field is student_info. The model refers to the content type in Strapi the field is pointing to. It is set to student_info and so this property in the Student content type points to the student_info content type.

We set the type as relation and the relation as oneToOne. All these state that the Student model has and belongs to one StudentInfo.

Let's see inside ./src/api/student-info/content-types/student-info/schema.json file

    {
      "kind": "collectionType",
      "collectionName": "student_infos",
      "info": {
        "singularName": "student-info",
        "pluralName": "student-infos",
        "displayName": "studentInfo"
      },
      "options": {
        "draftAndPublish": true
      },
      "pluginOptions": {},
      "attributes": {
        "bio": {
          "type": "text"
        },
        "student": {
          "type": "relation",
          "relation": "oneToOne",
          "target": "api::student.student",
          "inversedBy": "student_info"
        }
      }
    }

Here, we have a student property which points to the student collection type. The relation set here is also oneToOne

These two JSON configs of both Student and StudentInfo models establish a one-to-one relationship between them as you can see in the interface below. This is similar for all other relations.

Content Type builder modal showing field types

One-to-Many

Let's say we have two content types, Employee and Company. The Company has many Employee records, and the Employee record points back to a Company record. To establish this in the content types, we will go to their /schema.json files in our project and set relational fields in Strapi.

For the Company model, we want an employees relation to point to many Employees. So we will do the below in the ./src/api/company/content-types/company/schema.json file.

    {
    ...  
    "attributes": {
        "name": {
          "type": "string"
        },
        "employees": {
          "type": "relation",
          "relation": "oneToMany",
          "target": "api::employee.employee",
          "mappedBy": "company"
        }
      }
    }

Also, in ./src/api/employee/content-types/employee/schema.json file:

    {
    ...  
      "attributes": {
        "name": {
          "type": "string"
        },
        "company": {
          "type": "relation",
          "relation": "manyToOne",
          "target": "api::company.company",
          "inversedBy": "employees"
        }
      }
    }

This sets a one-to-many relationship in the Company model.

Many-to-Many

In setting a many-to-many relation from our Strapi project, we will set the relation field of both content types. For example, doctors can work in many hospitals and many hospitals can have many doctors. In this case, our Doctor model in ./src/api/doctor/content-types/doctor/schema.json will be this:

    {
      ...
      "attributes": {
        "name": {
          "type": "string"
        },
        "hospitals": {
          "type": "relation",
          "relation": "manyToMany",
          "target": "api::hospital.hospital",
          "inversedBy": "doctors"
        }
      }
    }

The hospital relation field points to many hospitals.

The Hospital model will be this: ./src/api/hospital/content-types/hospital/schema.json:

    {
      ...
      "attributes": {
        "name": {
          "type": "string"
        },
        "doctors": {
          "type": "relation",
          "relation": "manyToMany",
          "target": "api::doctor.doctor",
          "inversedBy": "hospitals"
        }
      }
    }

This effectively sets a many-to-many relation between the Doctor and Hospital models.

One-Way

To set this relation from our Strapi project between two models, we will define a relation field in one model's /schema.json file only. The other model will have no relation connecting to other model define in its /schema.json file.

For example, we have two models User and Detail and they have one-way relation. To set this up. We set the below in the User's model file user/models/user.settings.json file:

    {
      ...  
      "attributes": {
        "name": {
          "type": "string"
        },
        "details": {
          "type": "relation",
          "relation": "oneToOne", 
          "target": "api::detail.detail"
        }
      }
    }

There will be no relation setting in the Detail schema file that will point to the User model. So in this way, we have set a one-way relation between the User and Detail models in Strapi.

Many-Way

This is the same as the one-way relation, but this one involves one model pointing to many records in another model, but this other model does not point back. To set this manually in Strapi, we will set a relation field with the collection property in one model but no relation definition in the other model.

For example, a User has many Cars. The relation is many-way. A user can own many cars. The setting will be this for the User: user/models/user.settings.json:

    {
      ...  
      "attributes": {
        "name": {
          "type": "string"
        },
        "cars": {
          "type": "relation",
          "relation": "oneToMany",
          "target": "api::car.car"
        }
      }
    }

The car relation has a collection property that is set to car. This setting tells Strapi that the cars field in the User model points to many Car records. We will not make a relation in the Car model that will point back to the User model because this is a many-way relation.

We have learned all the relations in Strapi and also learned how to set them up both via the Strapi admin UI panel and from a Strapi project. Now, we show how to use some of the relations in Strapi to build a real-life app.

Setting up Strapi Project

We will create a Q&A app just like Quora, and users can ask questions, answer questions, and comment on answers. We will build this app to demonstrate how to use Strapi relations to link our models.

This project will be in two parts: the backend and the front-end. Of course, the backend will be built using Strapi, and the front-end will be built using Next.js.

We will create a central folder that will hold both backend and frontend projects:

mkdir relations

mkdir understanding-and-using-relations-in-strapi

We move into the folder:

cd relations

cd understanding-and-using-relations-in-strapi

Create the Strapi project:

npx create-strapi@latest

The CLI will ask a few more questions:

npx create-strapi@latest

 Strapi   v5.0.1 🚀 Let's create your new project

? What is the name of your project? qa-app
 
We can't find any auth credentials in your Strapi config.

Create a free account on Strapi Cloud and benefit from:

- Blazing-fast deployment for your projects
- Exclusive access to resources to make your project successful
- An Awesome community and full enjoyment of Strapi's ecosystem

Start your 14-day free trial now!


? Please log in or sign up. Skip
? Do you want to use the default database (sqlite) ? Yes
? Start with an example structure & data? No
? Start with Typescript? Yes
? Install dependencies with npm? Yes
? Initialize a git repository? Yes

 Strapi   Creating a new application at /Users/theodore/dev/qa-app

   deps   Installing dependencies with npm

The above command will create a Strapi project in qa-app folder inside the understanding-and-using-relations-in-strapi folder.

To start the project, run:

npm run develop

Strapi will serve the project on localhost:1337. It will launch the Strapi admin UI panel on localhost:1337/admin.

Strapi admin sign up page

Fill in your details and click on the LET'S START button. We will begin to build our collections but first, let's draw our models.

Models

We will have three models for our Q&A app. We will have Question, Answer and Comment. Our Question model will be this:

Question {
    qText
    user
}
  • qText: This will hold the question.
  • user: This holds the name of the user.

Strapi Question content type - Content-Type Builder _ Strapi

The Answer model will be this:

Answer {
    aText
    question
    user
}

Content-Type Builder _ Strapi · 1.32pm · 09-06

  • aText: This holds the answer text.
  • question: This holds the reference to the question.
  • user: The user that answered.

Answer content type Content-Type Builder _ Strapi · 1.32pm · 09-06 (1)

The Comment model will look like this:

Comment {
    cText
    answer
    user
}

Content-Type Builder _ Strapi · 1.32pm · 09-06

  • cText: This will hold the comment text on the answer.
  • answer: This is the reference to the answer.
  • user: The user that commented.

Comment content type - Content-Type Builder _ Strapi · 1.35pm · 09-06 (1)

We have seen how our collection will look like, now let's build our collections. These models have relationships that connect them. Let's see them below.

One-to-Many

The Question model and the Answer model have a one-to-many relationship. A Question will have many Answers. Now, we will build a Question collection in Strapi, and also we will create the Answer collection and there we will establish the relation between them. Now, on the http://localhost:1337/admin/ page click on the Create First Content Type button, a modal will appear. We will create the Question collection.

  • Type question in the Display name field.
  • Click on the text field.
  • Type qText in the Name field.
  • Select Long Text in the below radio button.

Question model - Content-Type Builder _ Strapi · 1.42pm · 09-06

  • Click on + Add another field.
  • Select text.
  • Type in user.

Question model - user field - Content-Type Builder _ Strapi · 1.43pm · 09-06

  • Click on Finish.
  • Next, click on the Save button on the top-right of the page.

Next, we will create the Answer collection

  • Click on the + Create new collection type link, a modal will show up, type in answer. Click on the + Add another field button.
  • Select text and type in aText.
  • Select Long Text

Answer model - Content-Type Builder _ Strapi · 1.46pm · 09-06

  • Click on + Add another field
  • Select text and type in user.

Content-Type Builder _ Strapi · 1.54pm · 09-06

  • Select relation field.
  • On the right box, press on the dropdown element and select Question.
  • Click on the fourth small box, counting from left. The box establishes a one-to-many relationship between the Question collection and the Answer collection.

Answer model - question one-to-many relation field - Content-Type Builder _ Strapi · 1.56pm · 09-06

  • Click on the Finish button.
  • Next, click on the Save button on the top-right of the page.

One-to-One

The Comment model and the Answer model have a one-to-one relationship. A comment has one answer. We will create the Comment collection.

  • Click on the + Create new collection type link, a modal will show up, type in comment.

Comment content type display name - Content-Type Builder _ Strapi · 2.00pm · 09-06

  • Click on the + Add another field button.
  • Select text field.
  • Type in cText and click on the + Add another field button.

Comment model - cText field - Content-Type Builder _ Strapi · 2.07pm · 09-06

  • Select relation field.
  • On the big box on the right, click on the dropdown element and select Answer.
  • Select the first small box, counting from the left. This box establishes the one-to-one relationship between the Comment and the Answer but not from Answer to comment. So, the comments field will not appear on the Answer response.

Comment relation field relations Content-Type Builder _ Strapi · 5.37pm · 09-07

  • Click on the Finish button.
  • Next, click on the Save button on the top-right of the page.

We are done building our collections and establishing their relationships. Now, let's build the front end.

Before we start building the frontend, we have set the permissions for a Public unauthenticated user so that our Strapi API can return data from routes without authentication.

NOTE: You’d typically need authentication in your application, especially when dealing with create, delete and update endpoints

Enable all permissions for answer collection type

Settings _ Roles _ Strapi

Enable all permissions for question collection type

Settings _ Roles _ Strapi · 2.14pm · 09-06

Enable all permissions for comment collection type

Settings _ Roles _ Strapi · 2.15pm · 09-06

Building the QnA App

Our app will have two pages: the index and the question view page.

  • / index: This page will display all questions in the app.
  • /questions/:id: This page is a dynamic page. It will display the details of a specific question. The details displayed are the answers to the question and the comments are replies to the answers.
npx create-next-app@latest

Then we complete some prompts:

npx create-next-app@latest qa-front
Need to install the following packages:
create-next-app@14.2.8
Ok to proceed? (y) y

 Would you like to use TypeScript? No / Yes
 Would you like to use ESLint? No / Yes
 Would you like to use Tailwind CSS? No / Yes
 Would you like to use `src/` directory? No / Yes
 Would you like to use App Router? (recommended) … No / Yes
 Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /Users/miracleio/Documents/writing/strapi/understanding-and-using-relations-in-strapi/qa-front.

Using npm.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next

npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated @humanwhocodes/config-array@0.11.14: Use @eslint/config-array instead
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported

added 368 packages, and audited 369 packages in 56s

139 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Initialized a git repository.

Success!

Now, we move into the directory:

cd qa-front

We will need the following dependencies:

  • axios: We will need this for making HTTP calls to our Strapi collection endpoints.
  • quill: An editor we will use for answering questions in our app.

We will install axios:

yarn add axios

npm install quill axios react-quill

Setting up Shadcn/UI

To speed up developmemnt we'll be leveraging shadcn/ui which is a collection of re-usable components that you can copy and paste into your apps.

npx shadcn@latest init

You will be asked a few questions to configure components.json:

Which style would you like to use? New York
Which color would you like to use as base color? Zinc
Do you want to use CSS variables for colors? no / yes

We can now start adding components to your project.

npx shadcn@latest add drawer button input label avatar textarea sonner

Complete the prompts:

npx shadcn@latest add drawer button input label avatar textarea
 You need to create a component.json file to add components. Proceed? yes
 Which style would you like to use? New York
? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit.
? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit.
? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit.
? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit.
? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit.
 Which color would you like to use as the base color? Slate
 Would you like to use CSS variables for theming? no / yes
 Writing components.json.
 Checking registry.
 Installing dependencies.
 Created 7 files:
  - components/ui/drawer.tsx
  - components/ui/button.tsx
  - components/ui/input.tsx
  - components/ui/label.tsx
  - components/ui/avatar.tsx
  - components/ui/textarea.tsx
  - components/ui/sonner.tsx

Fetching Data

Let's create the utility functions and type definnitions that we need to interact with the Strapi API.

Strapi v5 changes

The Strapi v5 API response format has been refined a bit from v4. Noteably, there's no longer an attributes to nest the incoming data.

Strapi 5 now uses documents and documents are accessed by their documentId.

Here's a sample of the data:

{
  "data": [
    {
      "id": 2,
      "documentId": "m2sdmnhihlkxolcvicrz4d4t",
      "qText": "How does Vibranium compare to Stark tech in terms of strength and versatility?",
      "user": "Happy Hogan",
      "createdAt": "2024-09-09T16:46:40.624Z",
      "updatedAt": "2024-09-09T16:46:40.624Z",
      "publishedAt": "2024-09-09T16:46:41.040Z",
      "locale": null
    },
    {
      "id": 4,
      "documentId": "a971959k5jbf7yqmxwgnrn02",
      "qText": "Could Tony Stark’s AI, JARVIS, have evolved into something beyond just an assistant?",
      "user": "Happy Hogan",
      "createdAt": "2024-09-09T18:39:34.602Z",
      "updatedAt": "2024-09-09T18:39:34.602Z",
      "publishedAt": "2024-09-09T18:39:34.980Z",
      "locale": null
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 1,
      "total": 2
    }
  }
}

Now, we'll create the types for our data response.

Creating Types

Create a new file ./types/index.ts:

mkdir types
touch types/index.ts

Enter the following:

type Comment = {
  id: number;
  documentId: string;
  cText: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  locale: null;
  user: string;
  answer: Answer;
};

type CommentInput = {
  cText: string;
  user: string;
  answer: string;
};

type Answer = {
  id: number;
  documentId: string;
  aText: string;
  user: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  locale: null;
  question?: Question | null;
};

type Question = {
  id: number;
  documentId: string;
  qText: string;
  user: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  locale: null;
  answers?: Answer[] | null;
};

type Meta = {
  pagination: {
    page: number;
    pageSize: number;
    pageCount: number;
    total: number;
  };
};

type AnswersResponse = {
  data?: Answer[];
  meta?: Meta;
};

type QuestionsResponse = {
  data?: Question[];
  meta?: Meta;
};

type CommentsResponse = {
  data?: Comment[];
  meta?: Meta;
};

type QuestionResponse = {
  data?: Question;
};

type AnswerResponse = {
  data?: Answer;
};

type CommentResponse = {
  data?: Comment;
};

type ErrorResponse = {
  error?: {
    message: string;
  };
};

export type {
  Answer,
  Question,
  Comment,
  CommentInput,
  Meta,
  QuestionsResponse,
  QuestionResponse,
  AnswersResponse,
  AnswerResponse,
  CommentsResponse,
  CommentResponse,
  ErrorResponse,
};

With these types we can confidently fetch data from our API and use it within our application.

Create Data fetching functions

Next, we'll create data fetching functions to GET and POST data.

Create a new file ./utils/index.ts:

mkdir utils
touch utils/index.ts

Enter the following:

// ./utils/index.ts

import {
  AnswerResponse,
  AnswersResponse,
  CommentInput,
  CommentResponse,
  CommentsResponse,
  ErrorResponse,
  QuestionResponse,
  QuestionsResponse,
} from "@/types";

const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;
const API_URL = process.env.NEXT_PUBLIC_API_URL;

/**
 * Fetches a list of questions from the API, sorted by their last update time in ascending order.
 *
 * @returns {Promise<QuestionsResponse & ErrorResponse>} - A promise that resolves with the list of questions and any errors encountered.
 */
const getQuestions: () => Promise<
  QuestionsResponse & ErrorResponse
> = async (): Promise<QuestionsResponse & ErrorResponse> => {
  try {
    const res = await fetch(`${API_URL}/questions?sort[0]=updatedAt:asc`, {
      headers: {
        Authorization: `Bearer ${API_TOKEN}`,
      },
      cache: "no-store",
    });
    const data = await res.json();
    console.log("🚀 ~ getQuestions ~ data", data);

    return data;
  } catch (error) {
    console.log("🚨 ~ getQuestions", error);
    return {
      error: { message: "Unable to fetch questions" },
    };
  }
};

/**
 * Creates a new question in the API.
 *
 * @param {Object} question - The question data.
 * @param {string} question.qText - The text of the question.
 * @param {string} question.user - The user ID of the person asking the question.
 * @returns {Promise<QuestionResponse & ErrorResponse>} - A promise that resolves with the created question and any errors encountered.
 */
const createQuestion = async (question: {
  qText: string;
  user: string;
}): Promise<QuestionResponse & ErrorResponse> => {
  console.log("🚀 ~ createQuestion ~ question", question);

  try {
    const res = await fetch(`${API_URL}/questions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_TOKEN}`,
      },
      body: JSON.stringify({
        data: {
          qText: question.qText,
          user: question.user,
        },
      }),
    });
    const data = await res.json();
    return data;
  } catch (error) {
    console.log("🚨 ~ createQuestion", error);
    return {
      error: { message: "Unable to create question" },
    };
  }
};

/**
 * Fetches comments related to a specific answer from the API, sorted by their last update time in ascending order.
 *
 * @param {string} answer - The ID of the answer to fetch comments for.
 * @returns {Promise<CommentsResponse & ErrorResponse>} - A promise that resolves with the list of comments and any errors encountered.
 */
const getComments = async (
  answer: string,
): Promise<CommentsResponse & ErrorResponse> => {
  try {
    const res = await fetch(
      `${API_URL}/comments?populate=*&filters[answer][documentId]=${answer}&sort[0]=updatedAt:asc`,
      {
        headers: {
          Authorization: `Bearer ${API_TOKEN}`,
        },
        cache: "no-store",
      },
    );
    const data = await res.json();
    if (data.error) {
      throw new Error(data.error.message);
    }
    return data;
  } catch (error) {
    console.log("🚨 ~ getComments", error);
    return {
      error: { message: "Unable to fetch comments" },
    };
  }
};

/**
 * Creates a new comment in the API.
 *
 * @param {CommentInput} comment - The comment data.
 * @returns {Promise<CommentResponse & ErrorResponse>} - A promise that resolves with the created comment and any errors encountered.
 */
const createComment: (
  comment: CommentInput,
) => Promise<CommentResponse & ErrorResponse> = async ({
  cText,
  user,
  answer,
}: CommentInput): Promise<CommentResponse & ErrorResponse> => {
  try {
    const res = await fetch(`${API_URL}/comments`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_TOKEN}`,
      },
      body: JSON.stringify({ data: { cText, user, answer } }),
    });
    const data = await res.json();
    if (data.error) {
      throw new Error(data.error.message);
    }
    return data;
  } catch (error) {
    console.log("🚨 ~ createComment", error);
    return {
      error: { message: "Unable to create comment" },
    };
  }
};

/**
 * Creates a new answer in the API.
 *
 * @param {Object} answer - The answer data.
 * @param {string} answer.aText - The text of the answer.
 * @param {string} answer.user - The user ID of the person providing the answer.
 * @param {string} answer.questionId - The ID of the question that the answer is related to.
 * @returns {Promise<AnswerResponse & ErrorResponse>} - A promise that resolves with the created answer and any errors encountered.
 */
const createAnswer = async (answer: {
  aText: string;
  user: string;
  questionId: string;
}): Promise<AnswerResponse & ErrorResponse> => {
  console.log("🚀 ~ createAnswer ~ answer", answer);

  try {
    const res = await fetch(`${API_URL}/answers`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_TOKEN}`,
      },
      body: JSON.stringify({
        data: {
          aText: answer.aText,
          user: answer.user,
          question: answer.questionId,
        },
      }),
    });
    const data = await res.json();
    return data;
  } catch (error) {
    console.log("🚨 ~ createAnswer", error);
    return {
      error: { message: "Unable to create answer" },
    };
  }
};

/**
 * Fetches a specific question from the API by its ID.
 *
 * @param {string} id - The ID of the question to fetch.
 * @returns {Promise<QuestionResponse & ErrorResponse>} - A promise that resolves with the question data and any errors encountered.
 */
const getQuestion: (
  id: string,
) => Promise<QuestionResponse & ErrorResponse> = async (id: string) => {
  try {
    const res = await fetch(`${API_URL}/questions/${id}?populate=*`, {
      headers: {
        Authorization: `Bearer ${API_TOKEN}`,
      },
      cache: "no-store",
    });
    const data = await res.json();
    if (data.error) {
      throw new Error(data.error.message);
    }
    return data;
  } catch (error) {
    console.log("🚨 ~ getQuestion", error);
    return {
      error: { message: "Unable to fetch question" },
    };
  }
};

/**
 * Fetches a list of answers related to a specific question from the API, sorted by their creation time in ascending order.
 *
 * @param {string} question - The ID of the question to fetch answers for.
 * @returns {Promise<AnswersResponse & ErrorResponse>} - A promise that resolves with the list of answers and any errors encountered.
 */
const getAnswers: (
  question: string,
) => Promise<AnswersResponse & ErrorResponse> = async (
  question: string,
): Promise<AnswersResponse & ErrorResponse> => {
  try {
    const res = await fetch(
      `${API_URL}/answers?populate=*&filters[question][documentId]=${question}&sort[0]=createdAt:asc`,
      {
        headers: {
          Authorization: `Bearer ${API_TOKEN}`,
        },
        cache: "no-store",
      },
    );
    const data = await res.json();
    if (data.error) {
      throw new Error(data.error.message);
    }
    return data;
  } catch (error) {
    console.log("🚨 ~ getAnswers", error);
    return {
      error: { message: "Unable to fetch answers" },
    };
  }
};

export {
  getQuestion,
  getQuestions,
  createQuestion,
  getAnswers,
  createAnswer,
  getComments,
  createComment,
};

This file defines several utility functions for interacting with an API that manages questions, answers, and comments. Here's a brief overview of each function:

  1. getQuestions: Fetches a list of questions from the API, sorted by the updatedAt field in ascending order. The sort parameter is passed as a query string:

    const res = await fetch(`${API_URL}/questions?sort[0]=updatedAt:asc`, {...});

    This ensures that the most recently updated questions are retrieved last.

  2. createQuestion: Sends a POST request to create a new question. The question's text and user ID are included in the request body, which is sent as JSON.

    body: JSON.stringify({ data: { qText: question.qText, user: question.user } }),
  3. getComments: Fetches comments related to a specific answer, filtering by the answer's documentId and sorting by the updatedAt field in ascending order. The filter and sort parameters are included in the query string:

    const res = await fetch(`${API_URL}/comments?populate=*&filters[answer][documentId]=${answer}&sort[0]=updatedAt:asc`, {...});
  4. createComment: Sends a POST request to create a new comment. The comment's text, user ID, and related answer ID are included in the request body, similar to createQuestion.

  5. createAnswer: Sends a POST request to create a new answer for a specific question. The answer's text, user ID, and the ID of the question it belongs to are included in the request body.

  6. getQuestion: Fetches details for a specific question by its ID, using the populate=* query parameter to include related data:

    const res = await fetch(`${API_URL}/questions/${id}?populate=*`, {...});
  7. getAnswers: Fetches a list of answers related to a specific question, filtering by the question document ID and sorting by the createdAt field in ascending order:

    const res = await fetch(`${API_URL}/answers?populate=*&filters[question][documentId]=${question}&sort[0]=createdAt:asc`, {...});

Each of these functions uses query parameters to filter and sort data returned by the API, ensuring that the correct data is retrieved in the desired order.

For example, the sort[0]=updatedAt:asc query parameter in getQuestions ensures that the list of questions is sorted by their update time, in ascending order. Similarly, the filters[answer][documentId]=${answer} parameter in getComments filters comments to only those related to a specific answer.

Note: The populate=* query parameter allows us to fetch all fields relation, media and components fields

Creating components

Now, we can create the components for displaying and posting data.

Question Card

Let's create a component for displaying questions - ./components/Question/Card.tsx and enter the following:

// ./components/Question/Card.tsx

import { Button } from "@/components/ui/button";
import { Question } from "@/types";
import Link from "next/link";

const QuestionCard: React.FC<{
  question: Question;
}> = ({ question }) => {
  return (
    <article
      key={question.id}
      className="rounded-none border border-stone-100 bg-stone-50 p-4 dark:border-stone-700 dark:bg-stone-800"
    >
      <h3 className="text-3xl font-semibold">{question.qText}</h3>
      <p className="text-stone-600 dark:text-stone-400">
        Asked by {question.user} on{" "}
        {new Date(question.createdAt).toDateString()}
      </p>
      <Button variant="outline" className="mt-3" asChild>
        <Link href={`/questions/${question.documentId}`}>View Question</Link>
      </Button>
    </article>
  );
};

export default QuestionCard;

This code defines a QuestionCard React component that displays a question's text, the user who asked it, and the date it was created. It includes a button that links to the detailed view of the question. The component uses Tailwind CSS classes for styling and receives a question object as a prop.

Question Form

Now, let's create the form component for creating new questions - ./components/Question/Form.tsxand enter the following:

// ./components/Question/Form.tsx

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createQuestion } from "@/utils";

const QuestionForm: React.FC<{
  className?: string;
}> = ({ className }) => {
  const [question, setQuestion] = useState("");
  const [name, setName] = useState("");
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!question.trim() || !name.trim()) {
      toast.error("Please fill in all fields");
      return;
    }
    toast.promise(createQuestion({ qText: question, user: name }), {
      loading: (() => {
        setLoading(true);
        return "Submitting question...";
      })(),
      success: (data) => {
        console.log("🚀 ~ handleSubmit ~ data", data);

        if (data.error) {
          throw new Error(data.error.message);
        }
        setLoading(false);
        setQuestion("");
        setName("");
        router.push(`/questions/${data.data?.documentId}`);

        return "Question submitted successfully!";
      },
      error: (error) => {
        setLoading(false);
        console.log("🚨 ~ handleSubmit ~ error", error);

        return "Failed to submit question";
      },
    });
  };
  return (
    <form
      onSubmit={handleSubmit}
      className={cn("grid items-start gap-4", className)}
    >
      <div className="grid gap-2">
        <Label htmlFor="name">Name</Label>
        <Input
          type="name"
          id="name"
          defaultValue="Happy Hogan"
          onChange={(e) => setName(e.target.value)}
          value={name}
        />
      </div>
      <div className="grid gap-2">
        <Label htmlFor="question">Your Question</Label>
        <Textarea
          id="question"
          value={question}
          onChange={(e) => setQuestion(e.target.value)}
        />
      </div>
      <Button type="submit">
        {loading ? "Submitting question..." : "Submit Question"}
      </Button>
    </form>
  );
};

export default QuestionForm;

The QuestionForm component allows users to submit a question. Here's a breakdown of its functionality:

  • It uses useState hooks to manage the state of the question, name, and loading fields.
  • The handleSubmit function:
    • Prevents the default form action.
    • Validates input fields.
    • Uses toast.promise to display loading, success, and error messages during the createQuestion API call.
    • Resets the form and redirects the user to the newly created question's page using router.push if the submission is successful.

Question Drawer

The Question form will be displayed in a drawer when the user wants to ask a new question. Create a new file - ./components/Question/Drawer.tsx:

// ./components/Question/Drawer.tsx

"use client";

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import QuestionForm from "@/components/Question/Form";

const QuestionDrawer: React.FC = () => {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <Drawer open={isOpen} onOpenChange={setIsOpen}>
      <DrawerTrigger asChild>
        <Button variant="outline">Ask Question</Button>
      </DrawerTrigger>
      <DrawerContent>
        <div className="wrapper mx-auto w-full max-w-3xl">
          <DrawerHeader className="text-left">
            <DrawerTitle>Ask Question</DrawerTitle>
            <DrawerDescription>What would you like to ask?</DrawerDescription>
          </DrawerHeader>
          {/* Question Form */}
          <QuestionForm className="px-4" />
          <DrawerFooter className="pt-2">
            <DrawerClose asChild>
              <Button variant="outline">Cancel</Button>
            </DrawerClose>
          </DrawerFooter>
        </div>
      </DrawerContent>
    </Drawer>
  );
};

export default QuestionDrawer;

Next, we'll create the components for Answers.

Answer Card

Create a new file - ./components/Answer/Card.tsx and enter the following:

// ./components/Answer/Card.tsx

"use client";

import { Answer, Comment } from "@/types";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import CommentForm from "@/components/Comment/Form";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import CommentCard from "@/components/Comment/Card";
import { getComments } from "@/utils";

const AnswerCard: React.FC<{
  answer: Answer;
}> = ({ answer }) => {
  const [showPostComment, setShowPostComment] = useState(false);
  const [showComments, setShowComments] = useState(false);
  const [comments, setComments] = useState<Comment[] | null>(null);
  const [loading, setLoading] = useState(false);

  const handleGetComments = async () => {
    if (showComments) {
      setShowComments(false);
      return;
    }
    setLoading(true);
    toast.promise(getComments(answer?.documentId), {
      loading: (() => {
        setLoading(true);
        return "Fetching comments...";
      })(),
      success: (data) => {
        if (data?.error) {
          return data.error.message;
        }
        if (!data?.data?.length) {
          return "No comments yet. Be the first!";
        }
        setComments(data?.data);
        setLoading(false);
        return "Comments fetched successfully";
      },
      error: (error) => {
        setLoading(false);
        console.log("🚨 ~ handleGetComments ~ error", error);
        return "Unable to fetch comments";
      },
      finally: () => {
        setLoading(false);
        setShowComments(true);
      },
    });
  };

  return (
    <article className="flex flex-col rounded-none border border-stone-100 bg-stone-50 dark:border-stone-900 dark:bg-stone-900">
      <div className="user flex w-full items-center gap-2 border-b border-stone-200 p-4 dark:border-stone-800">
        <Avatar>
          <AvatarImage
            src={`https://avatar.iran.liara.run/public/${Math.floor(
              Math.random() * 10 + 1,
            )}`}
            alt={answer?.user}
          />
          <AvatarFallback>
            {answer?.user
              ?.split(" ")
              .map((name) => name[0])
              .join("")}
          </AvatarFallback>
        </Avatar>
        <p className=" ">{answer?.user}</p> on{" "}
        <p>{new Date(answer?.updatedAt).toDateString()}</p>
      </div>
      <div
        className="p-4"
        {...{
          dangerouslySetInnerHTML: {
            __html: answer?.aText,
          },
        }}
      ></div>
      <div className="flex flex-wrap gap-2 border-t border-stone-200 p-4 dark:border-stone-800">
        {!showPostComment ? (
          <Button
            onClick={() => setShowPostComment(!showPostComment)}
            variant={"outline"}
          >
            Post a comment
          </Button>
        ) : (
          <div className="w-full">
            <Button
              onClick={() => setShowPostComment(!showPostComment)}
              variant={"outline"}
              className="mb-4"
            >
              Hide comment form
            </Button>
            <CommentForm answer={answer?.documentId} />
          </div>
        )}
        <Button onClick={handleGetComments} variant={"outline"}>
          {loading
            ? "Fetching comments..."
            : showComments
              ? "Hide comments"
              : "Show comments"}
        </Button>
      </div>
      {showComments && (
        <div className="border-t border-stone-200 dark:border-stone-800">
          {comments?.length ? (
            <ul className="flex flex-col gap-4">
              {comments.map((comment, i) => (
                <li
                  key={comment?.documentId}
                  className="border-t border-stone-200 first-of-type:border-t-0 dark:border-stone-800"
                >
                  <CommentCard comment={comment} i={i} />
                </li>
              ))}
            </ul>
          ) : (
            <p className="p-4">No comments yet. Be the first!</p>
          )}
        </div>
      )}
    </article>
  );
};

export default AnswerCard;

Here's how it works:

  • State Management: It uses useState hooks to manage the visibility of the comment form (showPostComment), the visibility of comments (showComments), the list of comments (comments), and the loading state (loading).
  • User Interface: The component renders an article element that displays the answer, along with the user who posted it and the date. It uses an Avatar component to show the user's avatar or initials.
  • Comment Handling:
    • When the "Post a comment" button is clicked, it toggles the visibility of the CommentForm by updating showPostComment.
    • When the "Show comments" button is clicked, it triggers handleGetComments, which fetches comments using the getComments function and updates the comments state. It also manages the loading state and displays a toast notification based on the success or failure of the operation.
  • Comment Display: If comments are fetched and showComments is true, the component renders a list of CommentCard components. If there are no comments, it displays a message encouraging the user to be the first to comment.

This structure allows users to interact with an answer by viewing or posting comments in a dynamic and responsive way.

Next, we'll create the form for posting answers:

Answer Form

Create a new file - ./components/Answer/Form.tsx and enter the following:

// ./components/Answer/Form.tsx

"use client";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { createAnswer } from "@/utils";

const AnswerForm: React.FC<{ id?: string }> = ({ id }) => {
  const router = useRouter();
  const [value, setValue] = useState("");
  const [name, setName] = useState("");
  const [loading, setLoading] = useState(false);
  console.log("🚀 ~ file: Form.tsx ~ line 6 ~ AnswerForm ~ value", value, id);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (!id) return toast.error("Invalid question ID");
    if (!value.trim() || !name.trim())
      return toast.error("Please fill in all fields before submitting");
    toast.promise(createAnswer({ aText: value, user: name, questionId: id }), {
      loading: (() => {
        setLoading(true);
        return "Submitting answer...";
      })(),
      success: (data) => {
        console.log("🚀 ~ handleSubmit ~ data", data);

        if (data.error) {
          throw new Error(data.error.message);
        }
        setLoading(false);
        setName("");
        setValue("");
        router.refresh();
        return "Answer submitted successfully!";
      },
      error: (error) => {
        console.log("🚨 ~ handleSubmit ~ error", error);
        setLoading(false);
        return error.message;
      },
    });
  };

  return (
    <>
      <form onSubmit={handleSubmit} className="flex flex-col gap-2">
        <ReactQuill
          theme="snow"
          value={value}
          onChange={(content, delta, source, editor) =>
            setValue(editor.getHTML())
          }
          placeholder="What's your answer?"
        />
        <Input
          type="text"
          placeholder="Your name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <Button type="submit" variant="default" className="w-fit">
          {loading ? "Submitting..." : "Submit Answer"}
        </Button>
      </form>
    </>
  );
};

export default AnswerForm;

The AnswerForm component provides a user interface for submitting answers to questions:

  • State Management: It manages value for the answer content, name for the user’s name, and loading to indicate the submission status.
  • Submission Handling: On form submission, it validates the presence of required fields and uses toast.promise to handle the createAnswer function, showing appropriate feedback.
  • UI Elements: It includes a ReactQuill editor for rich-text answers, an Input field for the user's name, and a Button that changes text based on the loading state.

The form clears fields and refreshes the page upon successful submission.

Next, we'll create the components for comments.

Comment Card

Create a new file - ./components/Comment/Card.tsx and enter the following:

// ./components/Comment/Card.tsx

"use client";

import { Comment } from "@/types";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

const CommentCard: React.FC<{
  comment: Comment;
  i: number;
}> = ({ comment, i }) => {
  return (
    <article className="flex flex-col gap-4 p-4">
      <div className="flex items-center gap-2">
        <Avatar>
          <AvatarImage
            src={`https://avatar.iran.liara.run/public/${i + 1}`}
            alt={comment?.user}
          />
          <AvatarFallback>
            {comment?.user
              ?.split(" ")
              .map((name) => name[0])
              .join("")}
          </AvatarFallback>
        </Avatar>
        <p className=" ">{comment?.user}</p>
      </div>
      <p className="text-sm">{comment?.cText}</p>
    </article>
  );
};

export default CommentCard;

The CommentCard component displays individual comments with the following features:

  • Avatar Display: Uses Avatar, AvatarImage, and AvatarFallback components to show a user's avatar, which is fetched from a URL based on the comment index (i + 1). If the avatar is not available, it displays the user's initials.
  • Comment Content: Shows the commenter's name and the text of the comment (comment.cText).

The component is styled with a flex layout for the avatar and text, ensuring a clean, organized appearance.

Comment Form

Create a new file - ./components/Comment/Form.tsx and enter the following:

// ./components/Comment/Form.tsx

"use client";

import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createComment } from "@/utils";

const CommentForm: React.FC<{
  answer: string;
}> = ({ answer }) => {
  const router = useRouter();
  const [comment, setComment] = useState("");
  const [user, setUser] = useState("");
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (!comment.trim() || !user.trim()) {
      toast.error("Please fill in all fields");
      return;
    }
    toast.promise(createComment({ cText: comment, user, answer }), {
      loading: (() => {
        setLoading(true);
        return "Posting comment...";
      })(),
      success: (data) => {
        console.log("🚀 ~ data", data);
        if (data.error) {
          throw new Error(data.error.message);
        }

        setLoading(false);
        setComment("");
        setUser("");
        router.refresh();
        return "Comment posted!";
      },
      error: (error) => {
        setLoading(false);
        return error.message;
      },
    });
  };

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-2">
      <Textarea
        className="bg-white dark:bg-stone-800"
        name="comment"
        placeholder="Type your comment here..."
        value={comment}
        onChange={(e) => setComment(e.target.value)}
      />
      <Input
        className="bg-white dark:bg-stone-800"
        type="text"
        name="name"
        placeholder="Your name"
        value={user}
        onChange={(e) => setUser(e.target.value)}
      />
      <Button type="submit" variant={"default"}>
        {loading ? "Posting comment..." : "Post comment"}
      </Button>
    </form>
  );
};

export default CommentForm;

The CommentForm component allows users to submit comments. Here's a brief breakdown:

  • Form Elements:
    • A Textarea for the comment text.
    • An Input field for the user's name.
    • A Button to submit the form.
  • State Management:
    • useState manages the comment text, user name, and loading state.
  • Form Submission:
    • On form submission, handleSubmit validates the inputs.
    • Uses the toast.promise function to handle the comment creation process and calls the createComment function, providing feedback on the submission status (loading, success, or error).
    • After a successful submission, it resets the form and refreshes the page using router.refresh().

This component provides a user-friendly interface for submitting comments.

Site Header Component

Next, we will create a SiteHeader component, this component will render our header so it appears in our app.

Run the below command to generate the Header files:

mkdir components/Site
touch components/Site/Header.tsx

Now, we open the components/Site/Header.tsx and paste the below code to it:

// ./components/Site/Header.tsx

import Link from "next/link";

const SiteHeader = () => {
  return (
    <header className="sticky top-0 z-10 w-full bg-red-700 p-4 text-red-50">
      <div className="wrapper mx-auto max-w-3xl">
        <Link href="/">
          <figure className="site-logo font-heading text-2xl font-black uppercase">
            The Q&A Times
          </figure>
        </Link>
      </div>
    </header>
  );
};

export default SiteHeader;

This component just renders the text The Q&A Times in the header section of our app.

Modifying the Layout

To make the component appear application-wide in our app we will go the the layout.tsx component in ./app/layout.tsx file and render the component.

// ./app/layout.tsx

import type { Metadata, Viewport } from "next";
import "./globals.css";
import SiteHeader from "@/components/Site/Header";
import { Toaster } from "@/components/ui/sonner";

const APP_NAME = "Q&A Times";
const APP_DEFAULT_TITLE = "The Q&A Times";
const APP_TITLE_TEMPLATE = "%s - Q&A Times";
const APP_DESCRIPTION =
  "Feel free to ask any question and get answers from the community";
const APP_URL = process.env.APP_URL || "https://qa-times.netlify.app";

export const metadata: Metadata = {
  applicationName: APP_NAME,
  title: {
    default: APP_DEFAULT_TITLE,
    template: APP_TITLE_TEMPLATE,
  },
  description: APP_DESCRIPTION,
  manifest: "/manifest.json",
  appleWebApp: {
    capable: true,
    statusBarStyle: "default",
    title: APP_DEFAULT_TITLE,
    // startUpImage: [],
  },
  formatDetection: {
    telephone: false,
  },
  openGraph: {
    type: "website",
    siteName: APP_NAME,
    title: {
      default: APP_DEFAULT_TITLE,
      template: APP_TITLE_TEMPLATE,
    },
    description: APP_DESCRIPTION,
    images: [
      {
        url: `${APP_URL}/images/qa-cover.png`,
        width: 1200,
        height: 630,
        alt: APP_DEFAULT_TITLE,
      },
    ],
  },
  twitter: {
    card: "summary",
    title: {
      default: APP_DEFAULT_TITLE,
      template: APP_TITLE_TEMPLATE,
    },
    description: APP_DESCRIPTION,
    images: [
      {
        url: `${APP_URL}/images/qa-cover.png`,
        width: 1200,
        height: 630,
        alt: APP_DEFAULT_TITLE,
      },
    ],
  },
};

export const viewport: Viewport = {
  themeColor: "#ffffff",
};
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <SiteHeader />
        {children}
        <Toaster richColors position="top-center" theme="system" />
      </body>
    </html>
  );
}

Here, we also defined the Metadata for our application and imported the <SiteHeader/>component. With this, our SiteHeader component will be rendered on all pages in our application.

Creating Pages

Let's create our page components.

Home Page

The ./app/page.tsx page will be loaded when the index route / is navigated to. This is our home page and will show questions and allow users to create new questions.

So, open the ./app/page.tsx file and paste the below code to it:

 // ./app/page.tsx

import QuestionCard from "@/components/Question/Card";
import QuestionDrawer from "@/components/Question/Drawer";
import { getQuestions } from "@/utils";

export default async function Home() {
  const questions = await getQuestions();
  return (
    <main>
      <header className="bg-stone-50 px-4 py-12 dark:bg-stone-900 lg:px-6">
        <div className="wrapper mx-auto max-w-3xl">
          <h1 className="mb-2 text-6xl font-black leading-tight">
            Start asking questions
          </h1>
          <QuestionDrawer />
        </div>
      </header>
      <section className="site-section p-4 lg:px-6">
        <div className="wrapper mx-auto max-w-3xl">
          <header className="section-header mb-4">
            <h2 className="section-title text-xl font-semibold">Questions</h2>
          </header>
          {questions?.data?.length ? (
            <ul className="grid gap-4">
              {questions.data.map((question) => (
                <li key={question?.documentId}>
                  <QuestionCard question={question} />
                </li>
              ))}
            </ul>
          ) : (
            <p>No questions found yet. Be the first to ask!</p>
          )}
        </div>
      </section>
    </main>
  );
}

Here, the getQuestions function is called to asynchronously fetch a list of questions. The page is divided into two main sections:

  1. Header: - Contains a title ("Start asking questions") and a QuestionDrawer component, likely a form or UI element to add new questions.
  2. Questions Section: - Displays the fetched questions using the QuestionCard component. - If there are no questions, it shows a message encouraging users to ask the first question.

Now, if the questions array is populated, it renders a list of QuestionCard components. If not, it displays a fallback message.

So, with that, we should have something like this:

image

Create Dynamic Question Page

In order to display the question and its answers we'll need to create a dynamic page using dynamic routes which will fetch questions by the ID and display it along with answers.

Create a new file - ./app/questions/[id]/page.tsxand enter the following:

// ./questions/[id]/page.tsx

import AnswerCard from "@/components/Answer/Card";
import AnswerForm from "@/components/Answer/Form";
import { getAnswers, getQuestion } from "@/utils";
import Link from "next/link";

const QuestionPage = async ({
  params,
}: {
  params: {
    id: string;
  };
}) => {
  // get the question id from the path
  const id = params.id;
  // fetch the question and answers
  const question = await getQuestion(id as string);
  const answers = await getAnswers(id as string);
  return (
    <main>
      {id && question.data ? (
        <>
          <header className="bg-stone-50 px-4 py-12 dark:bg-stone-900 lg:px-6">
            <div className="wrapper mx-auto max-w-3xl">
              <h1 className="mb-2 text-4xl font-black leading-tight">
                {question.data.qText}
              </h1>
              <p>
                Asked by {question.data.user} on{" "}
                {new Date(question.data.createdAt).toDateString()}
              </p>
            </div>
          </header>
          <section className="site-section bg-sla px-4 py-12 lg:px-6">
            <div className="wrapper mx-auto max-w-3xl">
              <AnswerForm id={id} />
            </div>
          </section>
          <section className="site-section px-4 py-12 lg:px-6">
            <div className="wrapper mx-auto max-w-3xl">
              <header className="section-header mb-8">
                <h2 className="text-2xl">Answers</h2>
              </header>
              <ul className="flex flex-col gap-4">
                {answers?.data?.length ? (
                  answers?.data?.map((answer) => (
                    <li className="" key={answer?.documentId}>
                      <AnswerCard answer={answer} />
                    </li>
                  ))
                ) : (
                  <p>No answers yet. Be the first!</p>
                )}
              </ul>
            </div>
          </section>
        </>
      ) : (
        <header className="bg-stone-50 px-4 py-12 dark:bg-stone-900 lg:px-6">
          <div className="wrapper mx-auto max-w-3xl">
            <h1 className="mb-2 text-4xl font-black leading-tight">
              Oops! Question not found
            </h1>
            <Link className="underline" href="/">
              Maybe you&apos;d like to ask a question?
            </Link>
          </div>
        </header>
      )}
    </main>
  );
};

export default QuestionPage;

This code sets up a dynamic page that shows a specific question and its answers based on the id in the URL. The id is pulled from the URL using params.id, and then it’s used to fetch the question and its related answers with getQuestion and getAnswers. If the question is found, the page displays the question details, an answer form, and a list of answers using the AnswerForm and AnswerCard components. If the question isn’t found, it shows a simple message saying the question wasn’t found and includes a link back to the homepage.

Test the App

Add new question:

The Q_A Times · 5.45pm · 09-09

New Question Added

The Q_A Times · 5.46pm · 09-09

View questions

The Q_A Times · 7.39pm · 09-09

Answer a question:

The Q_A Times · 7.42pm · 09-09

Submitted Answer

The Q_A Times · 7.43pm · 09-09 (1)

Comment on an answer:

The Q_A Times · 7.45pm · 09-09

Submitted Comment on an answer

The Q_A Times · 7.43pm · 09-09 (1)

Source Code and live preview

Find the source code of the project below:

Troubleshooting Relations

Encountering issues with relations is common. Here are some tips to identify and resolve them.

Common Issues and Solutions

  • Relations Not Linking Properly:

    • Verify the relation configurations in the Content-Type Builder.
    • Ensure that the populate parameter is used correctly when fetching data.
  • Null Values in Related Data:

    • Check that related records exist.
    • Confirm that the correct IDs are used when connecting relations.
  • Performance Issues:

    • Optimize queries by fetching only necessary fields.
    • Use pagination to limit the amount of data retrieved.

Debugging Approaches

  • Inspect API Responses: Use tools like Postman or your browser's developer tools to examine responses.
  • Enable Detailed Logging: Adjust logging levels in Strapi to get more information.
  • Test Queries in Isolation: Use the GraphQL Playground or REST client to test queries separately.

Error Handling Best Practices

  • Server-Side:

    • Implement error handling middleware.
    • Validate inputs and handle exceptions gracefully.
  • Client-Side:

    • Provide clear error messages to users.
    • Implement retry logic or fallback options when appropriate.

For more detailed information, consult the official Strapi documentation on relations and error handling.

Conclusion

Mastering relations in Strapi is essential for building dynamic and scalable applications. By understanding the different types of relations and how to implement them, you can create a robust content architecture that mirrors real-world connections. Remember to follow best practices, optimize performance, and handle errors gracefully to ensure your application is efficient and maintainable.

Looking to explore more about Strapi? Check out our articles on building custom controllers in Strapi and implementing authentication in Strapi.

Join our Newsletter

Get all the latest news and events.

Newsletter signup is not configured yet.

Authentication Methods in Strapi
APIs·11 min read

Guide on Authenticating Requests With the REST API in Strapi

This article was updated to Strapi v5.

·March 27, 2024
APIsBeginner·21 min read

How to Make API Calls in Android using Retrofit and Strapi CMS

Modern applications allow us to get connected to the world like never before. However, how is this structure so...

·October 13, 2023
A Deep Dive into Strapi GraphQL
APIsBeginner·17 min read

A Deep Dive into Strapi GraphQL

Learn how to set up GraphQL in Strapi, run GraphQL queries and mutations, carry out CRUD requests, and create an authentication system in Strapi.

·July 20, 2023