DEV Community

Strapi
Strapi

Posted on • Originally published at strapi.io

How to Build a Quiz App using a Strapi API with Angular

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 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. After which, you can install the Angular CLI by running:

    npm install -g @angular/cli 
Enter fullscreen mode Exit fullscreen mode

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

    npm i strapi -g
Enter fullscreen mode Exit fullscreen mode

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:

    npx create-strapi-app quiz-server --quickstart
Enter fullscreen mode Exit fullscreen mode

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.

However, you need to create an administrative user on the admin panel at 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
Enter fullscreen mode Exit fullscreen mode

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:

    {
      "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
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    {
      "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"
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

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" }
    ]
Enter fullscreen mode Exit fullscreen mode

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.

    // 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 };
        }
    };
Enter fullscreen mode Exit fullscreen mode

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

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

Making the API Endpoints Public

On the admin panel, 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:

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

Form to add a quiz

Generate and Setup the Angular App

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

    ng new quiz-app -S
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

    for module in core data "features/quiz --routing"; do ng g m $(printf %q "$module"); done
Enter fullscreen mode Exit fullscreen mode

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

    // src/environments/environment.ts
    export const environment = {
      production: false,
      strapiUrl: 'http://localhost:1337'
    };
Enter fullscreen mode Exit fullscreen mode

The Core Module

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

    ng g c core/components/header
    ng g c core/pages/not-found
Enter fullscreen mode Exit fullscreen mode

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 and 404 pages here. Remember to modify src/app/core/core.module.ts to this.

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 and generate them by running:

    for model in quiz question score user-answer; do ng g interface "data/models/${model}"; done
Enter fullscreen mode Exit fullscreen mode

The only service in this module is the quiz service. You can generate it by running:

    ng g s data/services/quiz
Enter fullscreen mode Exit fullscreen mode

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.

    // 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);
      }
    } 
Enter fullscreen mode Exit fullscreen mode

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

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

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:

    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
Enter fullscreen mode Exit fullscreen mode

You’ll be using bootstrap to style this app. So you’ll need to install ng-bootstrap.

    ng add @ng-bootstrap/ng-bootstrap
Enter fullscreen mode Exit fullscreen mode

Since the quiz will be a form, you’re going to need ReactiveFormsModule. This is what QuizModule should look like.

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

QuizRoutingModule should have three routes to the three pages.

    // 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 { }
Enter fullscreen mode Exit fullscreen mode

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.

    // src/app/features/quiz/components/title/title.component.ts
    export class TitleComponent {
      @Input() title = '';
      @Input() subtitle = '';
      constructor() { }
    }
Enter fullscreen mode Exit fullscreen mode

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 and its template here.

    // 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 });
      }
    }
Enter fullscreen mode Exit fullscreen mode

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 and its template here.

    // src/app/features/quiz/pages/quizzes/quizzes.component.ts
    export class QuizzesComponent implements OnInit {
      quizzes$ = this.quizService.getQuizzes();

      constructor(private quizService: QuizService) { }

      ngOnInit(): void {
      }
    }
Enter fullscreen mode Exit fullscreen mode

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

The Quizzes Page

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.

    // 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 });
      }
    }
Enter fullscreen mode Exit fullscreen mode

You can find the template for this component here. Here is a screenshot of what the page looks like.

The Quiz Page

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.

    // 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));
            })
          );
      }
    }
Enter fullscreen mode Exit fullscreen mode

You can find this component’s template here and its styling here. Here is a screenshot of this page.

The Score Page

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.

You’ll also need to add some universal styling to src/styles.css, which you can find here. Then all you need to do is run the app:

    ng serve
Enter fullscreen mode Exit fullscreen mode

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. If you would like to know more about Strapi, check out their documentation here.

Top comments (0)