Back to Blog
Tutorial

Add Quizzes to Your Laravel App with QuizAPI

Integrate QuizAPI into a Laravel application with the HTTP client, Blade templates, and proper form handling for a complete quiz experience.

Bobby Iliev2026-04-088 min read
Share:

Why QuizAPI + Laravel

Laravel ships with a powerful HTTP client, an expressive template engine, and session management out of the box. These three features are exactly what you need to build a quiz experience: fetch questions from QuizAPI, render them in Blade, and track answers across requests.

This tutorial walks you through building a fully functional quiz feature in an existing Laravel app. By the end, you will have a quiz listing page, a question-by-question flow, and a results screen with score calculation.

Prerequisites

  • PHP 8.2+ and Composer
  • Laravel 11 or later
  • A QuizAPI API key

Setting Up the API Client

Add your QuizAPI credentials to .env:

QUIZAPI_API_KEY=your_api_key_here QUIZAPI_BASE_URL=https://quizapi.io/api/v1

Register the config values in config/services.php:

'quizapi' => [ 'key' => env('QUIZAPI_API_KEY'), 'base_url' => env('QUIZAPI_BASE_URL', 'https://quizapi.io/api/v1'), ],

Create a dedicated service class at app/Services/QuizApiService.php:

1<?php 2 3namespace App\Services; 4 5use Illuminate\Support\Facades\Http; 6use Illuminate\Support\Facades\Cache; 7 8class QuizApiService 9{ 10 private string $baseUrl; 11 private string $apiKey; 12 13 public function __construct() 14 { 15 $this->baseUrl = config('services.quizapi.base_url'); 16 $this->apiKey = config('services.quizapi.key'); 17 } 18 19 public function getQuizzes(string $category = null): array 20 { 21 $cacheKey = 'quizzes_' . ($category ?? 'all'); 22 23 return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($category) { 24 $response = Http::withHeaders([ 25 'Authorization' => "Bearer {$this->apiKey}", 26 ])->get("{$this->baseUrl}/quizzes", array_filter([ 27 'category' => $category, 28 'limit' => 20, 29 ])); 30 31 $response->throw(); 32 33 return $response->json(); 34 }); 35 } 36 37 public function getQuiz(string $quizId): array 38 { 39 return Cache::remember("quiz_{$quizId}", now()->addMinutes(10), function () use ($quizId) { 40 $response = Http::withHeaders([ 41 'Authorization' => "Bearer {$this->apiKey}", 42 ])->get("{$this->baseUrl}/quizzes/{$quizId}"); 43 44 $response->throw(); 45 46 return $response->json(); 47 }); 48 } 49 50 public function submitAnswers(string $quizId, array $answers): array 51 { 52 $response = Http::withHeaders([ 53 'Authorization' => "Bearer {$this->apiKey}", 54 ])->post("{$this->baseUrl}/quizzes/{$quizId}/submit", [ 55 'answers' => $answers, 56 ]); 57 58 $response->throw(); 59 60 return $response->json(); 61 } 62}

Register the service in app/Providers/AppServiceProvider.php:

public function register(): void { $this->app->singleton(QuizApiService::class); }

Building the Controller

Create the quiz controller:

php artisan make:controller QuizController

Fill in the controller at app/Http/Controllers/QuizController.php:

1<?php 2 3namespace App\Http\Controllers; 4 5use App\Services\QuizApiService; 6use Illuminate\Http\Request; 7 8class QuizController extends Controller 9{ 10 public function __construct( 11 private QuizApiService $quizApi 12 ) {} 13 14 public function index(Request $request) 15 { 16 $category = $request->query('category'); 17 $quizzes = $this->quizApi->getQuizzes($category); 18 19 return view('quizzes.index', compact('quizzes', 'category')); 20 } 21 22 public function show(string $quizId) 23 { 24 $quiz = $this->quizApi->getQuiz($quizId); 25 26 return view('quizzes.show', compact('quiz')); 27 } 28 29 public function start(string $quizId, Request $request) 30 { 31 $quiz = $this->quizApi->getQuiz($quizId); 32 33 // Store quiz data and reset answers in session 34 $request->session()->put("quiz_{$quizId}", [ 35 'questions' => $quiz['questions'], 36 'answers' => [], 37 'current_index' => 0, 38 'started_at' => now()->toISOString(), 39 ]); 40 41 return redirect()->route('quizzes.question', [ 42 'quizId' => $quizId, 43 'index' => 0, 44 ]); 45 } 46 47 public function question(string $quizId, int $index, Request $request) 48 { 49 $session = $request->session()->get("quiz_{$quizId}"); 50 51 if (!$session) { 52 return redirect()->route('quizzes.show', $quizId); 53 } 54 55 $questions = $session['questions']; 56 57 if ($index < 0 || $index >= count($questions)) { 58 return redirect()->route('quizzes.show', $quizId); 59 } 60 61 $question = $questions[$index]; 62 $selectedAnswer = $session['answers'][$question['id']] ?? null; 63 64 return view('quizzes.question', [ 65 'quizId' => $quizId, 66 'question' => $question, 67 'index' => $index, 68 'total' => count($questions), 69 'selectedAnswer' => $selectedAnswer, 70 ]); 71 } 72 73 public function answer(string $quizId, int $index, Request $request) 74 { 75 $validated = $request->validate([ 76 'answer_id' => 'required|string', 77 ]); 78 79 $session = $request->session()->get("quiz_{$quizId}"); 80 $question = $session['questions'][$index]; 81 82 $session['answers'][$question['id']] = $validated['answer_id']; 83 $session['current_index'] = $index + 1; 84 $request->session()->put("quiz_{$quizId}", $session); 85 86 if ($index + 1 >= count($session['questions'])) { 87 return redirect()->route('quizzes.results', $quizId); 88 } 89 90 return redirect()->route('quizzes.question', [ 91 'quizId' => $quizId, 92 'index' => $index + 1, 93 ]); 94 } 95 96 public function results(string $quizId, Request $request) 97 { 98 $session = $request->session()->get("quiz_{$quizId}"); 99 100 if (!$session) { 101 return redirect()->route('quizzes.show', $quizId); 102 } 103 104 $result = $this->quizApi->submitAnswers($quizId, $session['answers']); 105 106 $request->session()->forget("quiz_{$quizId}"); 107 108 return view('quizzes.results', [ 109 'quizId' => $quizId, 110 'score' => $result['score'], 111 'total' => $result['total'], 112 'details' => $result['details'] ?? [], 113 ]); 114 } 115}

Defining Routes

Add the quiz routes in routes/web.php:

1use App\Http\Controllers\QuizController; 2 3Route::prefix('quizzes')->name('quizzes.')->group(function () { 4 Route::get('/', [QuizController::class, 'index'])->name('index'); 5 Route::get('/{quizId}', [QuizController::class, 'show'])->name('show'); 6 Route::post('/{quizId}/start', [QuizController::class, 'start'])->name('start'); 7 Route::get('/{quizId}/question/{index}', [QuizController::class, 'question'])->name('question'); 8 Route::post('/{quizId}/question/{index}', [QuizController::class, 'answer'])->name('answer'); 9 Route::get('/{quizId}/results', [QuizController::class, 'results'])->name('results'); 10});

Blade Templates

Create the question view at resources/views/quizzes/question.blade.php:

1<x-app-layout> 2 <div class="max-w-2xl mx-auto px-4 py-8"> 3 <div class="mb-4 text-sm text-gray-500"> 4 Question {{ $index + 1 }} of {{ $total }} 5 </div> 6 7 <div class="w-full bg-gray-200 rounded-full h-2 mb-6"> 8 <div 9 class="bg-blue-600 h-2 rounded-full" 10 style="width: {{ (($index + 1) / $total) * 100 }}%" 11 ></div> 12 </div> 13 14 <div class="bg-white rounded-lg shadow p-6"> 15 <h2 class="text-xl font-semibold mb-6">{{ $question['text'] }}</h2> 16 17 <form method="POST" action="{{ route('quizzes.answer', ['quizId' => $quizId, 'index' => $index]) }}"> 18 @csrf 19 20 <div class="space-y-3"> 21 @foreach ($question['answers'] as $answer) 22 <label 23 class="flex items-center p-4 rounded-lg border-2 cursor-pointer transition-colors 24 {{ $selectedAnswer === $answer['id'] ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' }}" 25 > 26 <input 27 type="radio" 28 name="answer_id" 29 value="{{ $answer['id'] }}" 30 {{ $selectedAnswer === $answer['id'] ? 'checked' : '' }} 31 class="mr-3" 32 /> 33 {{ $answer['text'] }} 34 </label> 35 @endforeach 36 </div> 37 38 @error('answer_id') 39 <p class="mt-2 text-sm text-red-600">{{ $message }}</p> 40 @enderror 41 42 <div class="flex justify-between mt-6"> 43 @if ($index > 0) 44 <a 45 href="{{ route('quizzes.question', ['quizId' => $quizId, 'index' => $index - 1]) }}" 46 class="px-4 py-2 bg-gray-100 rounded-lg" 47 > 48 Previous 49 </a> 50 @else 51 <div></div> 52 @endif 53 54 <button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg"> 55 {{ $index + 1 === $total ? 'Submit Quiz' : 'Next' }} 56 </button> 57 </div> 58 </form> 59 </div> 60 </div> 61</x-app-layout>

Create the results view at resources/views/quizzes/results.blade.php:

1<x-app-layout> 2 <div class="max-w-2xl mx-auto px-4 py-8 text-center"> 3 <h1 class="text-3xl font-bold mb-4">Quiz Complete</h1> 4 5 <div class="text-6xl font-bold text-blue-600 mb-2"> 6 {{ $score }}/{{ $total }} 7 </div> 8 9 <p class="text-gray-600 mb-8"> 10 You scored {{ round(($score / $total) * 100) }}% 11 </p> 12 13 @if (count($details) > 0) 14 <div class="text-left space-y-4 mt-8"> 15 @foreach ($details as $detail) 16 <div class="bg-white rounded-lg shadow p-4"> 17 <div class="flex items-start gap-3"> 18 <span class="{{ $detail['correct'] ? 'text-green-500' : 'text-red-500' }}"> 19 {{ $detail['correct'] ? '✓' : '✗' }} 20 </span> 21 <div> 22 <p class="font-medium">{{ $detail['question'] }}</p> 23 @if (!$detail['correct'] && isset($detail['explanation'])) 24 <p class="text-sm text-gray-600 mt-1">{{ $detail['explanation'] }}</p> 25 @endif 26 </div> 27 </div> 28 </div> 29 @endforeach 30 </div> 31 @endif 32 33 <a href="{{ route('quizzes.index') }}" class="inline-block mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg"> 34 Browse More Quizzes 35 </a> 36 </div> 37</x-app-layout>

Error Handling

Wrap API calls with proper error handling so users see friendly messages instead of stack traces:

1// In QuizApiService.php, add a retry mechanism 2public function getQuiz(string $quizId): array 3{ 4 return Cache::remember("quiz_{$quizId}", now()->addMinutes(10), function () use ($quizId) { 5 $response = Http::withHeaders([ 6 'Authorization' => "Bearer {$this->apiKey}", 7 ]) 8 ->retry(3, 100) 9 ->get("{$this->baseUrl}/quizzes/{$quizId}"); 10 11 if ($response->status() === 404) { 12 abort(404, 'Quiz not found'); 13 } 14 15 if ($response->status() === 429) { 16 abort(503, 'Quiz service is temporarily unavailable. Please try again.'); 17 } 18 19 $response->throw(); 20 21 return $response->json(); 22 }); 23}

Summary

You now have a complete quiz flow in Laravel: listing, question-by-question navigation with session-backed state, and a results page. The QuizApiService handles all API communication with caching and retries, keeping your controllers clean.

Next steps to consider:

  • Add authentication so users can track their quiz history
  • Store results in a local database for analytics
  • Build an admin panel to manage quiz assignments
  • Add timed quizzes using the started_at session timestamp
Test Your Knowledge

Think you understand Tutorial? Put your skills to the test with hands-on quiz questions.

Tutorial
Start Practicing

Enjoyed this article?

Share it with your team or try our quiz platform.

Stay Updated

Get the latest tutorials and API tips delivered to your inbox.

No spam, unsubscribe anytime.