How to Build a Quiz App using a Strapi API with Angular
Published: 2023-02-01 09:11:07

This tutorial is a guide on how to create a quiz app. The app will use Strapi as a backend server and Angular in the frontend. 

A range of quizzes will be provided in the app. Users of the app should be shown a list of quizzes on the home page. When they select a quiz, a list of questions should be displayed, each with four answer choices. Once they answer all the questions and submit them, a score page should indicate how they performed on it. 

This score breakdown should contain the overall number of questions they got right. It should also point out which questions they got wrong and their correct answers. 

The app will use Strapi as a [backend](https://masteringbackend.com/posts/backend-development-the-ultimate-guide) since it automatically generates an API. It also provides an admin panel where you can enter content types. 

This significantly cuts down on time needed to build an API server as you don’t have to build it from scratch. Strapi is a headless content management system (CMS). With it, you can create and manage content as well as have APIs generated for them. 

It’s open-source, supports user management and permissions, REST, GraphQL, several databases, and internationalization. By following this tutorial, you will learn how to set up Strapi and use it with an Angular application. 

To begin, you will set up the Strapi server. After the setup, you will create two content types and modify permissions to make their APIs public. You will also add some data on the admin panel.

Next, you will generate the Angular app. It will have 3 main pages: the quizzes page, an individual quiz page, and a score page. Lastly, you will create an HTTP quiz service for the Strapi API and integrate it with these pages. 

By the end of this tutorial, you will have created a quiz app that will give you a selection of quizzes, allow you to answer questions on a quiz, and provide results for attempted quizzes. 

## Prerequisites

To follow along with this tutorial, you need to have Node.js, and the Angular CLI installed. You can install Node.js using one of its installers [found on its downloads page](https://nodejs.org/en/download/). After which, you can install the Angular CLI by running:

```bash
    npm install -g @angular/cli 
```

The Strapi CLI is optional but can help generate models faster. You can install it by running:

```bash
    npm i strapi -g
```

## Setting up the Strapi Server

The server will be called `quiz-server`. To generate the server, you will need to run the quickstart installation script as follows:

```bash
    npx create-strapi-app quiz-server --quickstart
```

This will create a **quiz-server** folder in the directory where you run this script. This script will also launch the server and make it available at [http://localhost:1337](http://localhost:1337). 

However, you need to create an administrative user on the admin panel at [http://localhost:1337/admin](http://localhost:1337/admin) and log in before creating content types.

# Creating Content Types

Next, you’ll create two content types: `quiz` and `question`. The quiz model will have three attributes: `name`, `description`, and `questions`. The `question` model will have seven: `text`, `a`, `b`, `c`, `d`, `answer,` and `quizzes`. 

The last attributes of each model will be relations connecting the two. The other attributes for both models will be text/strings. 

While the server is still running, run the following commands in another terminal to generate the quiz and question APIs: 

```
    strapi generate:api quiz name:string description:text
    strapi generate:api question text:text a:string b:string c:string d:string answer:string
```

The above commands will generate models, controllers, services, and config for each content type. However, you’ll still need to add the `quizzes` attribute to the **Question** model and specify its relationship to the **Quiz** model. 

It should have a many-to-many relationship to **Quizzes**. You’ll add it in the `/api/question/models/question.settings.json` file. You’ll also make all the attributes required. 

It’s also important to make the `answer` attribute a private field so that it is not included when the API returns questions. It should look something like this:

```json
    {
      "kind": "collectionType",
      "collectionName": "questions",
      "info": {
        "name": "question",
        "description": ""
      },
      "options": {
        "draftAndPublish": true,
        "timestamps": true,
        "increments": true,
        "comment": ""
      },
      "attributes": {
        "text": {
          "type": "text",
          "required": true
        },
        "a": {
          "type": "string",
          "required": true
        },
        "b": {
          "type": "string",
          "required": true
        },
        "c": {
          "type": "string",
          "required": true
        },
        "d": {
          "type": "string",
          "required": true
        },
        "answer": {
          "type": "string",
          "private": true,
          "required": true
        },
        "quizzes": {
          "collection": "quiz",
          "via": "questions",
          "dominant": true
        }
      }
    }
```

You’ll also add a `questions` attribute to the **Quiz** model and make all its attributes required. This will be in the `api/quiz/models/quiz.settings.json` file. 

```json
    {
      "kind": "collectionType",
      "collectionName": "quizzes",
      "info": {
        "name": "quiz",
        "description": ""
      },
      "options": {
        "draftAndPublish": true,
        "timestamps": true,
        "increments": true,
        "comment": ""
      },
      "attributes": {
        "name": {
          "type": "string",
          "required": true
        },
        "description": {
          "type": "text",
          "required": true
        },
        "questions": {
          "via": "quizzes",
          "collection": "question"
        }
      }
    }
```

Creating this relationship makes it easier to assign a question to a quiz and vice versa when creating them on the admin panel. When adding new content, you can select whether to add a question to a quiz and vice versa on the creation form. 

The many-to-many relationship also makes it possible to share questions among multiple quizzes and limit one question to one quiz.

## Adding a Route to Score Quizzes

To grade a completed quiz, you need a new route. It should be available at `/quizzes/:id/score` and should be a `POST` method. It should also accept a body that is structured as follows:

```
    [
          { "questionId": 1, "value": "A" },
          { "questionId": 2, "value": "B" }
    ]
```

You’ll add the controller for this route in `api/quiz/controllers/quiz.js`. In this controller, the quiz corresponding to the provided id is fetched. 

Then the answers provided are compared to the answers to the quiz’s questions. An answer is marked correct or wrong, and the number of correct answers is tracked.

```js
    // api/quiz/controllers/quiz.js
    'use strict';
    
    module.exports = {
        async score(ctx) {
            const { id } = ctx.params;
            let userAnswers = ctx.request.body;
    
            let quiz = await strapi.services.quiz.findOne({ id }, ['questions']);
    
            let question;
            let score = 0;
    
            if (quiz) {
                userAnswers.map((userAnsw) => {
                    question = quiz.questions.find((qst) => qst.id === userAnsw.questionId);
                    if (question) {
                        if (question.answer === userAnsw.value) {
                            userAnsw.correct = true;
                            score += 1;
                        } else {
                            userAnsw.correct = false;
                        }
    
                        userAnsw.correctValue = question.answer;
                    }
    
                    return userAnsw;
                });
            }
    
            const questionCount = quiz.questions.length;
    
            delete quiz.questions;
    
            return { quiz, score, scoredAnswers: userAnswers, questionCount };
        }
    };
```

Lastly, add a route for the controller to `api/quiz/config/routes.json`. 

```json
    // api/quiz/config/routes.json
    {
      "routes": [
        ... ,
        {
          "method": "POST",
          "path": "/quizzes/:id/score",
          "handler": "quiz.score",
          "config": {
            "policies": []
          }
        }
      ]
    }
```

## Making the API Endpoints Public

On the [admin panel](http://localhost:1337/admin/), you’ll need to make a couple of quiz routes public. Under **General > Settings > Users & Permissions Plugin > Roles > Public > Permissions** check the **find, find one** , and **score** actions for the **Quiz** content type. 

This will make the `/quizzes`, `/quizzes/:id`, and `/quizzes/:id/score` routes of the API public. Here’s what that will look like:


![](https://paper-attachments.dropbox.com/s_9684EB005499F41CC71B4AD3D30EDCF9D85DCCCCA249DC3418C02C69B2B4C12F_1626768110496_permissions.png)


Once done, click the **Save** button to save the changes. Before you can test the API, you need to add new content. Create a couple of questions and quizzes under **Collection Types > Questions > Add New Questions** and **Collection Types > Quizzes > Add New Quizzes**. 

Note that you can add questions to quizzes and vice versa on the forms. Once finished, publish the quizzes and questions.  


![Form to add a question](https://paper-attachments.dropbox.com/s_9684EB005499F41CC71B4AD3D30EDCF9D85DCCCCA249DC3418C02C69B2B4C12F_1626720008451_Screen+Shot+2021-07-19+at+9.36.04+PM.png)

![Form to add a quiz](https://paper-attachments.dropbox.com/s_9684EB005499F41CC71B4AD3D30EDCF9D85DCCCCA249DC3418C02C69B2B4C12F_1626720043213_Screen+Shot+2021-07-19+at+9.36.24+PM.png)

## Generate and Setup the Angular App

The frontend portion of the app will be called `quiz-app`. To generate it, run:

```bash
    ng new quiz-app -S
```

Pick CSS for styling and add routing to the app when prompted. 

This will be the structure of the app:


    src/app
    ├── core
    │   ├── components
    │   └── pages
    ├── data
    │   ├── models
    │   └── services
    └── features
        └── quiz
            ├── components
            └── pages

The app is comprised of four modules: core, data, quiz, and quiz routing. The core module will contain everything central to the app, like headers, 404 pages, error pages, etc. 

The data module will hold all the models and services you’ll use to connect to Strapi. The feature modules folder will hold all the modules related to features. 

For now, since you’ll only be focused on the quiz, it will just contain the quiz module. However, if you choose to add authentication to the app, you could add an auth module here. The quiz routing module will be responsible for routing to the quiz pages.

To generate the four modules run:

```bash
    for module in core data "features/quiz --routing"; do ng g m $(printf %q "$module"); done
```


To connect to the Strapi server, you need to set its API URL in the environment file `src/environments/environment.ts`.

```typescript
    // src/environments/environment.ts
    export const environment = {
      production: false,
      strapiUrl: 'http://localhost:1337'
    };
```

## The Core Module

This module will contain the app header and the 404 pages. You can generate these components by running:

```bash
    ng g c core/components/header
    ng g c core/pages/not-found
```


Since these are not the main part of the app, they will not be touched on as much. You can find the header component [here](https://github.com/zaracooper/quiz-app/tree/main/src/app/core/components/header) and 404 pages [here](https://github.com/zaracooper/quiz-app/tree/main/src/app/core/pages/not-found). Remember to modify `src/app/core/core.module.ts` to [this](https://github.com/zaracooper/quiz-app/blob/main/src/app/core/core.module.ts). 

# The Data Module

This module will contain four models and one service. The four models will be the `Quiz`, `Question`, `Score`, and `UserAnswer`. 

The `Quiz` and `Question` models reflect the content types you created earlier. The Score represents the results returned once a quiz is graded. 

The `UserAnswer` model denotes the answers a user provides to quiz questions. You can find each of the models [here](https://github.com/zaracooper/quiz-app/tree/main/src/app/data/models) and generate them by running:

```bash
    for model in quiz question score user-answer; do ng g interface "data/models/${model}"; done
```


The only service in this module is the [q](https://github.com/zaracooper/quiz-app/blob/main/src/app/data/services/quiz.service.ts)[uiz service](https://github.com/zaracooper/quiz-app/blob/main/src/app/data/services/quiz.service.ts). You can generate it by running: 

```bash
    ng g s data/services/quiz
```

It will make HTTP calls to the Strapi server using the quiz routes you made public. It will have three methods: `getQuizzes` to get all quizzes, `getQuiz` to get a particular quiz, and `score` to grade a user’s answers.

```typescript
    // src/app/data/services/quiz.service.ts
    @Injectable({
      providedIn: 'root'
    })
    export class QuizService {
      private url = `${environment.strapiUrl}/quizzes`;
    
      constructor(private http: HttpClient) { }
    
      getQuizzes() {
        return this.http.get<Quiz[]>(this.url);
      }
      getQuiz(id: number) {
        return this.http.get<Quiz>(`${this.url}/${id}`);
      }
      score(id: number, answers: UserAnswer[]) {
        return this.http.post<Score>(`${this.url}/${id}/score`, answers);
      }
    } 
```


Since you’re going to make HTTP calls from this service, you’ll need to add `HttpClientModule` to `AppModule`.

```typescript
    // src/app/app.module.ts
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule,
        HttpClientModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
```

# The Quiz Module

This module will contain 2 components and 3 pages. The question component will display the question and its multiple answers. The title component will display the quiz name and description on the other 3 pages. 

The pages include the quizzes page, which lists all available quizzes, the quiz page where you take the quiz, and the score page where the results are displayed. To generate them, run:

```bash
    for comp in question title; do ng g c "features/quiz/components/${comp}"; done
    for page in quiz quizzes score; do ng g c "features/quiz/pages/${page}"; done
```


You’ll be using bootstrap to style this app. So you’ll need to install [ng-bootstrap](https://github.com/ng-bootstrap/ng-bootstrap). 

```bash
    ng add @ng-bootstrap/ng-bootstrap
```


Since the quiz will be a form, you’re going to need `ReactiveFormsModule`. This is what [QuizModule](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/quiz.module.ts) should look like. 

```typescript
    // src/app/features/quiz/quiz.module.ts
    @NgModule({
      declarations: [
        QuestionComponent,
        QuizzesComponent,
        QuizComponent,
        ScoreComponent,
        TitleComponent
      ],
      imports: [
        CommonModule,
        QuizRoutingModule,
        NgbModule,
        ReactiveFormsModule
      ]
    })
    export class QuizModule { }
```

`QuizRoutingModule` should have three routes to the three pages. 

```typescript
    // src/app/features/quiz/quiz-routing.module.ts
    const routes: Routes = [
        { path: '', component: QuizzesComponent },
        { path: 'quiz/:id', component: QuizComponent },
        { path: 'quiz/:id/score', component: ScoreComponent }
    ];
    
    @NgModule({
        imports: [RouterModule.forChild(routes)],
        exports: [RouterModule]
    })
    export class QuizRoutingModule { }
```

## The Title Component

This component will display the quiz app title and description on the aforementioned pages. As such, it needs to take the quiz title and description as input. You can find the template for this component [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/components/title/title.component.html). 
 
```typescript
    // src/app/features/quiz/components/title/title.component.ts
    export class TitleComponent {
      @Input() title = '';
      @Input() subtitle = '';
      constructor() { }
    }
```

## The Question Component

This component will display the question. So it needs to take a question and the question’s number as input. The `question` and `number` properties will handle that. It also has to output an answer when a user clicks a choice. 

That’s what the `setAnswer` property will do. When a user picks an answer, the `pickAnswer` method is called, and `setAnswer` emits an event with the selected choice. You can find the styling for this component [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/components/question/question.component.css) and its template [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/components/question/question.component.html). 
 
```typescript
    // src/app/features/quiz/components/question/question.component.ts
    export class QuestionComponent {
      @Input() question = {} as Question;
      @Input() number = 0;
    
      @Output() setAnswer = new EventEmitter<UserAnswer>();
    
      selectedAnswer = '';
    
      constructor() { }
    
      pickAnswer(id: number, answer: string, value: string) {
        this.selectedAnswer = `[${answer}] ${value}`;
        this.setAnswer.emit({ questionId: id, value: answer });
      }
    }
```

## The Quizzes Page

This is the landing page. Here is where a list of available quizzes will be displayed. You’ll fetch the quizzes from the `QuizService` and store them in the `quizzes$` property. You can find the styling for this component [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/pages/quizzes/quizzes.component.css) and its template [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/pages/quizzes/quizzes.component.html). 

```typescript
    // src/app/features/quiz/pages/quizzes/quizzes.component.ts
    export class QuizzesComponent implements OnInit {
      quizzes$ = this.quizService.getQuizzes();
    
      constructor(private quizService: QuizService) { }
    
      ngOnInit(): void {
      }
    }
```

Here is a screenshot of what this page will look like:

![The Quizzes Page](https://paper-attachments.dropbox.com/s_9684EB005499F41CC71B4AD3D30EDCF9D85DCCCCA249DC3418C02C69B2B4C12F_1626786074276_Screen+Shot+2021-07-20+at+4.00.58+PM.png)

## The Quiz Page

This is the page where a user will take the quiz. When the component is initialized, you’ll get the quiz id from the route using the `ActivatedRoute` service. Using this `id`, you’ll fetch the quiz from `QuizService`. 

The `quizForm` property will be the form group model for the quiz form. When the quiz response is received, you will loop through each question, create a form control for each, and add them to the form group. 

A hidden input will be added for each question to the template and will track its answer. The submit button is disabled until all questions are answered, and the form is valid. 

The `setValue` method assigns the answer it receives from the `QuestionComponent` to the form control that matches the question id. When the submit button is clicked, the `score` method is triggered, and the value of the form is sent to the score page. 

```typescript
    // src/app/features/quiz/pages/quiz/quiz.component.ts
    export class QuizComponent implements OnInit, OnDestroy {
      quiz!: Quiz;
      quizSub!: Subscription;
      quizForm: FormGroup = new FormGroup({});
      quizId = 0;
    
      constructor(private quizService: QuizService, private route: ActivatedRoute, private router: Router) { }
    
      ngOnDestroy(): void {
        this.quizSub.unsubscribe();
      }
    
      ngOnInit(): void {
        this.quizSub = this.route.paramMap.pipe(
          switchMap(params => {
            this.quizId = Number(params.get('id'));
            return this.quizService.getQuiz(this.quizId);
          })
        ).subscribe(
          quiz => {
            this.quiz = quiz;
    
            quiz.questions.forEach(question => {
              this.quizForm.addControl(question.id.toString(), new FormControl('', Validators.required));
            });
          }
        );
      }
    
      setAnswerValue(answ: UserAnswer) {
        this.quizForm.controls[answ.questionId].setValue(answ.value);
      }
    
      score() {
        this.router.navigateByUrl(`/quiz/${this.quizId}/score`, { state: this.quizForm.value });
      }
    }
```

You can find the template for this component [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/pages/quiz/quiz.component.html). Here is a screenshot of what the page looks like. 


![The Quiz Page](https://paper-attachments.dropbox.com/s_9684EB005499F41CC71B4AD3D30EDCF9D85DCCCCA249DC3418C02C69B2B4C12F_1626788867268_Screen+Shot+2021-07-20+at+4.47.32+PM.png)

## The Score Page

On this page, the results of the quiz are displayed. When the component is initialized, the quiz id and the user’s answers are retrieved using the `ActivatedRoute` service. 

A request is then made to grade the answers using the `QuizService`. The results of the grading are stored in the `score$` property. 

```typescript
    // src/app/features/quiz/pages/score/score.component.ts
    export class ScoreComponent implements OnInit {
      score$: Observable<Score> | undefined;
      quizId = 0;
    
      constructor(private route: ActivatedRoute, private quizService: QuizService) { }
      
    ngOnInit(): void {
        this.score$ = this.route.paramMap
          .pipe(
            switchMap(params => {
              const state = window.history.state;
              this.quizId = Number(params.get('id'));
    
              let reqBody: UserAnswer[] = [];
    
              for (const [qstId, answ] of Object.entries(state)) {
                if (typeof answ === 'string') {
                  reqBody.push({ questionId: Number(qstId), value: answ });
                }
              }
    
              return iif(() => this.quizId > 0 && reqBody.length > 0, this.quizService.score(this.quizId, reqBody));
            })
          );
      }
    }
```


You can find this component’s template [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/pages/score/score.component.html) and its styling [here](https://github.com/zaracooper/quiz-app/blob/main/src/app/features/quiz/pages/score/score.component.css). Here is a screenshot of this page. 


![The Score Page](https://paper-attachments.dropbox.com/s_9684EB005499F41CC71B4AD3D30EDCF9D85DCCCCA249DC3418C02C69B2B4C12F_1626789298010_Screen+Shot+2021-07-20+at+4.54.39+PM.png)

## Tying Things Up

One of the last things you’ll need to do is add routes to the quiz module and 404 pages. You’ll do this in the `AppRoutingModule` file at `src/app/app-routing.module.ts`. 

Another thing you’ll need to do is to remove the placeholder content from the app component template and add the header to it. It should look like [this](https://github.com/zaracooper/quiz-app/blob/main/src/app/component/app.component.html). 

You’ll also need to add some universal styling to `src/styles.css`, which you can find [here](https://github.com/zaracooper/quiz-app/blob/main/src/styles.css). Then all you need to do is run the app:

```bash
    ng serve
```

## Conclusion

By the end of this tutorial, you will have built a quiz app with Strapi and Angular. You will have generated an API that supplies quizzes and questions using Strapi. 

Additionally, you will have created an Angular app that consumes data from this API. The app should contain three main pages to list quizzes, allow users to take quizzes, and show the results of a graded quiz. 

You can find the source code for this app [here](https://github.com/zaracooper/quiz-app). If you would like to know more about Strapi, check out their documentation [here](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html).