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.
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
The Strapi CLI is optional but can help generate models faster. You can install it by running:
npm i strapi -g
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
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.
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:
1 strapi generate:api quiz name:string description:text
2 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:
1 {
2 "kind": "collectionType",
3 "collectionName": "questions",
4 "info": {
5 "name": "question",
6 "description": ""
7 },
8 "options": {
9 "draftAndPublish": true,
10 "timestamps": true,
11 "increments": true,
12 "comment": ""
13 },
14 "attributes": {
15 "text": {
16 "type": "text",
17 "required": true
18 },
19 "a": {
20 "type": "string",
21 "required": true
22 },
23 "b": {
24 "type": "string",
25 "required": true
26 },
27 "c": {
28 "type": "string",
29 "required": true
30 },
31 "d": {
32 "type": "string",
33 "required": true
34 },
35 "answer": {
36 "type": "string",
37 "private": true,
38 "required": true
39 },
40 "quizzes": {
41 "collection": "quiz",
42 "via": "questions",
43 "dominant": true
44 }
45 }
46 }
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.
1 {
2 "kind": "collectionType",
3 "collectionName": "quizzes",
4 "info": {
5 "name": "quiz",
6 "description": ""
7 },
8 "options": {
9 "draftAndPublish": true,
10 "timestamps": true,
11 "increments": true,
12 "comment": ""
13 },
14 "attributes": {
15 "name": {
16 "type": "string",
17 "required": true
18 },
19 "description": {
20 "type": "text",
21 "required": true
22 },
23 "questions": {
24 "via": "quizzes",
25 "collection": "question"
26 }
27 }
28 }
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.
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:
1 [
2 { "questionId": 1, "value": "A" },
3 { "questionId": 2, "value": "B" }
4 ]
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.
1 // api/quiz/controllers/quiz.js
2 'use strict';
3
4 module.exports = {
5 async score(ctx) {
6 const { id } = ctx.params;
7 let userAnswers = ctx.request.body;
8
9 let quiz = await strapi.services.quiz.findOne({ id }, ['questions']);
10
11 let question;
12 let score = 0;
13
14 if (quiz) {
15 userAnswers.map((userAnsw) => {
16 question = quiz.questions.find((qst) => qst.id === userAnsw.questionId);
17 if (question) {
18 if (question.answer === userAnsw.value) {
19 userAnsw.correct = true;
20 score += 1;
21 } else {
22 userAnsw.correct = false;
23 }
24
25 userAnsw.correctValue = question.answer;
26 }
27
28 return userAnsw;
29 });
30 }
31
32 const questionCount = quiz.questions.length;
33
34 delete quiz.questions;
35
36 return { quiz, score, scoredAnswers: userAnswers, questionCount };
37 }
38 };
Lastly, add a route for the controller to api/quiz/config/routes.json
.
1 // api/quiz/config/routes.json
2 {
3 "routes": [
4 ... ,
5 {
6 "method": "POST",
7 "path": "/quizzes/:id/score",
8 "handler": "quiz.score",
9 "config": {
10 "policies": []
11 }
12 }
13 ]
14 }
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.
The frontend portion of the app will be called quiz-app
. To generate it, run:
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:
1src/app
2├── core
3│ ├── components
4│ └── pages
5├── data
6│ ├── models
7│ └── services
8└── features
9 └── quiz
10 ├── components
11 └── 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:
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
.
1 // src/environments/environment.ts
2 export const environment = {
3 production: false,
4 strapiUrl: 'http://localhost:1337'
5 };
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
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.
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
The only service in this module is the quiz service. You can generate it by running:
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.
1 // src/app/data/services/quiz.service.ts
2 @Injectable({
3 providedIn: 'root'
4 })
5 export class QuizService {
6 private url = `${environment.strapiUrl}/quizzes`;
7
8 constructor(private http: HttpClient) { }
9
10 getQuizzes() {
11 return this.http.get<Quiz[]>(this.url);
12 }
13 getQuiz(id: number) {
14 return this.http.get<Quiz>(`${this.url}/${id}`);
15 }
16 score(id: number, answers: UserAnswer[]) {
17 return this.http.post<Score>(`${this.url}/${id}/score`, answers);
18 }
19 }
Since you’re going to make HTTP calls from this service, you’ll need to add HttpClientModule
to AppModule
.
1 // src/app/app.module.ts
2 @NgModule({
3 declarations: [
4 AppComponent
5 ],
6 imports: [
7 BrowserModule,
8 AppRoutingModule,
9 HttpClientModule
10 ],
11 providers: [],
12 bootstrap: [AppComponent]
13 })
14 export class AppModule { }
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
You’ll be using bootstrap to style this app. So you’ll need to install ng-bootstrap.
ng add @ng-bootstrap/ng-bootstrap
Since the quiz will be a form, you’re going to need ReactiveFormsModule
. This is what QuizModule should look like.
1 // src/app/features/quiz/quiz.module.ts
2 @NgModule({
3 declarations: [
4 QuestionComponent,
5 QuizzesComponent,
6 QuizComponent,
7 ScoreComponent,
8 TitleComponent
9 ],
10 imports: [
11 CommonModule,
12 QuizRoutingModule,
13 NgbModule,
14 ReactiveFormsModule
15 ]
16 })
17 export class QuizModule { }
QuizRoutingModule
should have three routes to the three pages.
1 // src/app/features/quiz/quiz-routing.module.ts
2 const routes: Routes = [
3 { path: '', component: QuizzesComponent },
4 { path: 'quiz/:id', component: QuizComponent },
5 { path: 'quiz/:id/score', component: ScoreComponent }
6 ];
7
8 @NgModule({
9 imports: [RouterModule.forChild(routes)],
10 exports: [RouterModule]
11 })
12 export class QuizRoutingModule { }
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.
1 // src/app/features/quiz/components/title/title.component.ts
2 export class TitleComponent {
3 @Input() title = '';
4 @Input() subtitle = '';
5 constructor() { }
6 }
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.
1 // src/app/features/quiz/components/question/question.component.ts
2 export class QuestionComponent {
3 @Input() question = {} as Question;
4 @Input() number = 0;
5
6 @Output() setAnswer = new EventEmitter<UserAnswer>();
7
8 selectedAnswer = '';
9
10 constructor() { }
11
12 pickAnswer(id: number, answer: string, value: string) {
13 this.selectedAnswer = `[${answer}] ${value}`;
14 this.setAnswer.emit({ questionId: id, value: answer });
15 }
16 }
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.
1 // src/app/features/quiz/pages/quizzes/quizzes.component.ts
2 export class QuizzesComponent implements OnInit {
3 quizzes$ = this.quizService.getQuizzes();
4
5 constructor(private quizService: QuizService) { }
6
7 ngOnInit(): void {
8 }
9 }
Here is a screenshot of what this page will look like:
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.
1 // src/app/features/quiz/pages/quiz/quiz.component.ts
2 export class QuizComponent implements OnInit, OnDestroy {
3 quiz!: Quiz;
4 quizSub!: Subscription;
5 quizForm: FormGroup = new FormGroup({});
6 quizId = 0;
7
8 constructor(private quizService: QuizService, private route: ActivatedRoute, private router: Router) { }
9
10 ngOnDestroy(): void {
11 this.quizSub.unsubscribe();
12 }
13
14 ngOnInit(): void {
15 this.quizSub = this.route.paramMap.pipe(
16 switchMap(params => {
17 this.quizId = Number(params.get('id'));
18 return this.quizService.getQuiz(this.quizId);
19 })
20 ).subscribe(
21 quiz => {
22 this.quiz = quiz;
23
24 quiz.questions.forEach(question => {
25 this.quizForm.addControl(question.id.toString(), new FormControl('', Validators.required));
26 });
27 }
28 );
29 }
30
31 setAnswerValue(answ: UserAnswer) {
32 this.quizForm.controls[answ.questionId].setValue(answ.value);
33 }
34
35 score() {
36 this.router.navigateByUrl(`/quiz/${this.quizId}/score`, { state: this.quizForm.value });
37 }
38 }
You can find the template for this component here. Here is a screenshot of what the page looks like.
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.
1 // src/app/features/quiz/pages/score/score.component.ts
2 export class ScoreComponent implements OnInit {
3 score$: Observable<Score> | undefined;
4 quizId = 0;
5
6 constructor(private route: ActivatedRoute, private quizService: QuizService) { }
7
8 ngOnInit(): void {
9 this.score$ = this.route.paramMap
10 .pipe(
11 switchMap(params => {
12 const state = window.history.state;
13 this.quizId = Number(params.get('id'));
14
15 let reqBody: UserAnswer[] = [];
16
17 for (const [qstId, answ] of Object.entries(state)) {
18 if (typeof answ === 'string') {
19 reqBody.push({ questionId: Number(qstId), value: answ });
20 }
21 }
22
23 return iif(() => this.quizId > 0 && reqBody.length > 0, this.quizService.score(this.quizId, reqBody));
24 })
25 );
26 }
27 }
You can find this component’s template here and its styling here. Here is a screenshot of this page.
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
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.
Zara is a software developer and technical writer, using React Native, React, Rails, Node, Golang, Angular and many more technologies.