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.