Connecting a Flutter App to a Laravel API

  • Flutter
  • Laravel
  • Mobile

How to connect a Flutter app to a Laravel API: Sanctum token authentication, API resources, error handling, versioning and secure token storage.

Flutter for the app and Laravel for the backend is a productive pairing. I have used it for HR and sales applications on both iOS and Android. Most of the problems teams hit are not in either framework but in the contract between them. This is the setup I start from.

Authentication with Laravel Sanctum

For a mobile app, use Sanctum's API tokens rather than cookie sessions. The app sends credentials once, receives a token and sends it as a Bearer header on every request.

// routes/api.php
Route::post('/v1/login', function (Request $request) {
    $request->validate(['email' => 'required|email', 'password' => 'required', 'device_name' => 'required']);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages(['email' => ['The credentials are incorrect.']]);
    }

    return ['token' => $user->createToken($request->device_name)->plainTextToken];
});

Naming the token after the device lets a user, or an administrator, revoke one lost phone without logging out everywhere.

Store the token safely

On the device, keep the token in flutter_secure_storage, which uses the Keychain on iOS and the Keystore on Android. Do not put it in shared preferences, where it sits in plain text.

One HTTP client with interceptors

Create a single Dio instance for the whole app. An interceptor adds the token to every request and handles a 401 response in one place by clearing the session and returning the user to the login screen.

dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) async {
    final token = await storage.read(key: 'token');
    if (token != null) options.headers['Authorization'] = 'Bearer $token';
    options.headers['Accept'] = 'application/json';
    handler.next(options);
  },
  onError: (error, handler) async {
    if (error.response?.statusCode == 401) await session.signOut();
    handler.next(error);
  },
));

The Accept: application/json header matters. Without it, Laravel may answer a validation failure with a redirect instead of a 422 JSON response.

A stable response shape

Use Laravel API Resources so the JSON does not change every time a database column does. On the Flutter side, parse responses into typed model classes at the edge of the app, so a missing field fails in one place with a clear message.

Version from day one

Unlike a website, old versions of a mobile app stay in use for months. Prefix routes with /v1 and never make a breaking change inside a version. Add an endpoint that tells the app its minimum supported version, so you can ask users to update when you truly must. The wider rules are in REST API design for mobile sales apps.

Plan for bad connections

Set timeouts, show a clear message when a request fails and let the user retry. If the app is used in the field, go further and design it offline-first.

Common questions

How do I authenticate a Flutter app with a Laravel backend?

Use Laravel Sanctum API tokens. The app posts the user's credentials to a login endpoint, receives a token, stores it in secure storage and sends it as a Bearer token on each request.

Where should a Flutter app store an API token?

In flutter_secure_storage, which uses the iOS Keychain and Android Keystore. Avoid shared preferences because values there are stored in plain text.

Why does my Laravel API return HTML or a redirect to Flutter?

The request is probably missing the Accept: application/json header. Add it to every request so Laravel returns JSON errors such as 422 and 401.