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

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_at session timestamp

Stay Updated

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

No spam, unsubscribe anytime.

Enjoyed this article?

Share it with your team or try our quiz platform.