Strapi
Published in

Strapi

Build a Quiz App using a Strapi API with Angular

In this tutorial you will build a quiz app with Strapi and Angular and generate an API that supplies quizzes and questions using Strapi. And create an Angular app that consumes data from this API.

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
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:

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

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.

strapi generate:api quiz name:string description:text
strapi generate:api question text:text a:string b:string c:string d:string answer:string
{
"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
}
}
}
{
"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"
}
}
}

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" }
]
// 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 };
}
};
// 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, 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.

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
src/app
├── core
│ ├── components
│ └── pages
├── data
│ ├── models
│ └── services
└── features
└── quiz
├── components
└── pages
for module in core data "features/quiz --routing"; do ng g m $(printf %q "$module"); done
// 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:

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

The Data Module

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

for model in quiz question score user-answer; do ng g interface "data/models/${model}"; done
ng g s data/services/quiz
// 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);
}
}
// 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.

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
ng add @ng-bootstrap/ng-bootstrap
// src/app/features/quiz/quiz.module.ts
@NgModule({
declarations: [
QuestionComponent,
QuizzesComponent,
QuizComponent,
ScoreComponent,
TitleComponent
],
imports: [
CommonModule,
QuizRoutingModule,
NgbModule,
ReactiveFormsModule
]
})
export class QuizModule { }
// 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.

// 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.

// 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 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 {
}
}
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.

// 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 });
}
}
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.

// 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));
})
);
}
}
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.

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.

--

--

Strapi is the leading open-source headless CMS. It’s 100% Javascript, fully customizable and developer-first. Unleash your content with Strapi.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store