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:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class QuizApiService
{
private string $baseUrl;
private string $apiKey;
public function __construct()
{
$this->baseUrl = config('services.quizapi.base_url');
$this->apiKey = config('services.quizapi.key');
}
public function getQuizzes(string $category = null): array
{
$cacheKey = 'quizzes_' . ($category ?? 'all');
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($category) {
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
])->get("{$this->baseUrl}/quizzes", array_filter([
'category' => $category,
'limit' => 20,
]));
$response->throw();
return $response->json();
});
}
public function getQuiz(string $quizId): array
{
return Cache::remember("quiz_{$quizId}", now()->addMinutes(10), function () use ($quizId) {
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
])->get("{$this->baseUrl}/quizzes/{$quizId}");
$response->throw();
return $response->json();
});
}
public function submitAnswers(string $quizId, array $answers): array
{
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
])->post("{$this->baseUrl}/quizzes/{$quizId}/submit", [
'answers' => $answers,
]);
$response->throw();
return $response->json();
}
}
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:
<?php
namespace App\Http\Controllers;
use App\Services\QuizApiService;
use Illuminate\Http\Request;
class QuizController extends Controller
{
public function __construct(
private QuizApiService $quizApi
) {}
public function index(Request $request)
{
$category = $request->query('category');
$quizzes = $this->quizApi->getQuizzes($category);
return view('quizzes.index', compact('quizzes', 'category'));
}
public function show(string $quizId)
{
$quiz = $this->quizApi->getQuiz($quizId);
return view('quizzes.show', compact('quiz'));
}
public function start(string $quizId, Request $request)
{
$quiz = $this->quizApi->getQuiz($quizId);
// Store quiz data and reset answers in session
$request->session()->put("quiz_{$quizId}", [
'questions' => $quiz['questions'],
'answers' => [],
'current_index' => 0,
'started_at' => now()->toISOString(),
]);
return redirect()->route('quizzes.question', [
'quizId' => $quizId,
'index' => 0,
]);
}
public function question(string $quizId, int $index, Request $request)
{
$session = $request->session()->get("quiz_{$quizId}");
if (!$session) {
return redirect()->route('quizzes.show', $quizId);
}
$questions = $session['questions'];
if ($index < 0 || $index >= count($questions)) {
return redirect()->route('quizzes.show', $quizId);
}
$question = $questions[$index];
$selectedAnswer = $session['answers'][$question['id']] ?? null;
return view('quizzes.question', [
'quizId' => $quizId,
'question' => $question,
'index' => $index,
'total' => count($questions),
'selectedAnswer' => $selectedAnswer,
]);
}
public function answer(string $quizId, int $index, Request $request)
{
$validated = $request->validate([
'answer_id' => 'required|string',
]);
$session = $request->session()->get("quiz_{$quizId}");
$question = $session['questions'][$index];
$session['answers'][$question['id']] = $validated['answer_id'];
$session['current_index'] = $index + 1;
$request->session()->put("quiz_{$quizId}", $session);
if ($index + 1 >= count($session['questions'])) {
return redirect()->route('quizzes.results', $quizId);
}
return redirect()->route('quizzes.question', [
'quizId' => $quizId,
'index' => $index + 1,
]);
}
public function results(string $quizId, Request $request)
{
$session = $request->session()->get("quiz_{$quizId}");
if (!$session) {
return redirect()->route('quizzes.show', $quizId);
}
$result = $this->quizApi->submitAnswers($quizId, $session['answers']);
$request->session()->forget("quiz_{$quizId}");
return view('quizzes.results', [
'quizId' => $quizId,
'score' => $result['score'],
'total' => $result['total'],
'details' => $result['details'] ?? [],
]);
}
}
Defining Routes
Add the quiz routes in routes/web.php:
use App\Http\Controllers\QuizController;
Route::prefix('quizzes')->name('quizzes.')->group(function () {
Route::get('/', [QuizController::class, 'index'])->name('index');
Route::get('/{quizId}', [QuizController::class, 'show'])->name('show');
Route::post('/{quizId}/start', [QuizController::class, 'start'])->name('start');
Route::get('/{quizId}/question/{index}', [QuizController::class, 'question'])->name('question');
Route::post('/{quizId}/question/{index}', [QuizController::class, 'answer'])->name('answer');
Route::get('/{quizId}/results', [QuizController::class, 'results'])->name('results');
});
Blade Templates
Create the question view at resources/views/quizzes/question.blade.php:
<x-app-layout>
<div class="max-w-2xl mx-auto px-4 py-8">
<div class="mb-4 text-sm text-gray-500">
Question {{ $index + 1 }} of {{ $total }}
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mb-6">
<div
class="bg-blue-600 h-2 rounded-full"
style="width: {{ (($index + 1) / $total) * 100 }}%"
></div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-6">{{ $question['text'] }}</h2>
<form method="POST" action="{{ route('quizzes.answer', ['quizId' => $quizId, 'index' => $index]) }}">
@csrf
<div class="space-y-3">
@foreach ($question['answers'] as $answer)
<label
class="flex items-center p-4 rounded-lg border-2 cursor-pointer transition-colors
{{ $selectedAnswer === $answer['id'] ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' }}"
>
<input
type="radio"
name="answer_id"
value="{{ $answer['id'] }}"
{{ $selectedAnswer === $answer['id'] ? 'checked' : '' }}
class="mr-3"
/>
{{ $answer['text'] }}
</label>
@endforeach
</div>
@error('answer_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
<div class="flex justify-between mt-6">
@if ($index > 0)
<a
href="{{ route('quizzes.question', ['quizId' => $quizId, 'index' => $index - 1]) }}"
class="px-4 py-2 bg-gray-100 rounded-lg"
>
Previous
</a>
@else
<div></div>
@endif
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-lg">
{{ $index + 1 === $total ? 'Submit Quiz' : 'Next' }}
</button>
</div>
</form>
</div>
</div>
</x-app-layout>
Create the results view at resources/views/quizzes/results.blade.php:
<x-app-layout>
<div class="max-w-2xl mx-auto px-4 py-8 text-center">
<h1 class="text-3xl font-bold mb-4">Quiz Complete</h1>
<div class="text-6xl font-bold text-blue-600 mb-2">
{{ $score }}/{{ $total }}
</div>
<p class="text-gray-600 mb-8">
You scored {{ round(($score / $total) * 100) }}%
</p>
@if (count($details) > 0)
<div class="text-left space-y-4 mt-8">
@foreach ($details as $detail)
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-start gap-3">
<span class="{{ $detail['correct'] ? 'text-green-500' : 'text-red-500' }}">
{{ $detail['correct'] ? '✓' : '✗' }}
</span>
<div>
<p class="font-medium">{{ $detail['question'] }}</p>
@if (!$detail['correct'] && isset($detail['explanation']))
<p class="text-sm text-gray-600 mt-1">{{ $detail['explanation'] }}</p>
@endif
</div>
</div>
</div>
@endforeach
</div>
@endif
<a href="{{ route('quizzes.index') }}" class="inline-block mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg">
Browse More Quizzes
</a>
</div>
</x-app-layout>
Error Handling
Wrap API calls with proper error handling so users see friendly messages instead of stack traces:
// In QuizApiService.php, add a retry mechanism
public function getQuiz(string $quizId): array
{
return Cache::remember("quiz_{$quizId}", now()->addMinutes(10), function () use ($quizId) {
$response = Http::withHeaders([
'Authorization' => "Bearer {$this->apiKey}",
])
->retry(3, 100)
->get("{$this->baseUrl}/quizzes/{$quizId}");
if ($response->status() === 404) {
abort(404, 'Quiz not found');
}
if ($response->status() === 429) {
abort(503, 'Quiz service is temporarily unavailable. Please try again.');
}
$response->throw();
return $response->json();
});
}
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
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.
Enjoyed this article?
Share it with your team or try our quiz platform.