Reader Stacks

Custom Login and Registration in Laravel: Standard and AJAX-Based

Building auth manually rather than scaffolding it gives full control over the form and flow — the AJAX version follows the same controller logic, returning JSON instead of a redirect.

Custom Login and Registration in Laravel: Standard and AJAX-Based

Building login and registration manually, rather than using a full scaffolding package, gives complete control over the form markup and validation flow — genuinely useful when a project's design or requirements don't fit a pre-built auth starter kit's assumptions.

The registration route and controller method

Route::get('/register', [AuthController::class, 'showRegister']);
Route::post('/register', [AuthController::class, 'register']);
public function register(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|min:8|confirmed',
    ]);

    $user = User::create([
        'name' => $validated['name'],
        'email' => $validated['email'],
        'password' => Hash::make($validated['password']),
    ]);

    Auth::login($user);

    return redirect('/dashboard');
}

The login route and controller method

Route::get('/login', [AuthController::class, 'showLogin']);
Route::post('/login', [AuthController::class, 'login']);
public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (Auth::attempt($credentials, $request->boolean('remember'))) {
        $request->session()->regenerate();
        return redirect()->intended('dashboard');
    }

    return back()->withErrors(['email' => 'Invalid credentials.']);
}

Logging out

public function logout(Request $request)
{
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();

    return redirect('/');
}

Invalidating the session and regenerating the CSRF token on logout, not just calling Auth::logout() alone, is what fully clears the previous session's state — skipping these two calls can leave remnants of the old session active.

The same registration flow, handled via AJAX instead

public function register(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users,email',
        'password' => 'required|min:8|confirmed',
    ]);

    $user = User::create([
        'name' => $validated['name'],
        'email' => $validated['email'],
        'password' => Hash::make($validated['password']),
    ]);

    Auth::login($user);

    return response()->json(['redirectUrl' => '/dashboard']);
}
$('#register-form').submit(function (event) {
    event.preventDefault();

    $.ajax({
        url: '/register',
        method: 'POST',
        data: $(this).serialize(),
        success: function (response) {
            window.location.href = response.redirectUrl;
        },
        error: function (xhr) {
            const errors = xhr.responseJSON.errors;
            $.each(errors, function (field, messages) {
                $(`#${field}-error`).text(messages[0]);
            });
        }
    });
});

The controller logic itself barely changes — only the final return statement differs, returning JSON instead of a redirect response, following the same jQuery AJAX redirect pattern covered elsewhere on this site for handling the success case client-side.

Why validation errors return automatically as JSON for an AJAX request

Laravel automatically detects an AJAX request (via the X-Requested-With header jQuery sets by default) and returns validation failures as a JSON 422 response instead of a redirect-with-errors — this is exactly why the error callback above can reliably read xhr.responseJSON.errors without any special server-side handling needed to detect the request type.

Why building this manually still makes sense despite Laravel's starter kits

Laravel's official starter kits (Breeze, Jetstream) scaffold a complete, well-tested auth system quickly — building it manually as shown here remains worthwhile specifically when a project's design, validation rules, or flow genuinely diverge enough from the starter kit's assumptions that customizing the scaffolded code would take more effort than building the specific flow needed from scratch.