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.
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_atsession timestamp
Think you understand Tutorial? Put your skills to the test with hands-on quiz questions.
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.
Related Articles
How to Build a Quiz App with Django and QuizAPI
Step-by-step guide to building a quiz application with Django using the QuizAPI REST API. Fetch questions, render a quiz UI, and submit scores.
Building a Quiz Component in React with QuizAPI
Build a reusable React quiz component that fetches questions from QuizAPI, manages quiz state, and displays scores. Full TypeScript implementation included.
Building a Quiz Leaderboard with Real-Time Updates
Build a live quiz leaderboard with ranking algorithms, efficient data models, and real-time delivery using SSE and WebSockets.