Type-safe & elegant: how to integrate GraphQL into Flutter

We like to build beautiful things. And we like to build them right. When we wanted to connect a Flutter app with a GraphQL API, it was clear to us that it shouldn't just work - it should also be fun. Developers should get autocompletion, type safety and direct verification against the API schema. What did we use for this? graphql_flutter, graphql_codegen and a little bit of magic.
Why GraphQL at all?
Because we only want to get what we need - no more, no less. GraphQL allows exactly that: finely granulated data queries via a single interface.
In case you're not quite sure what GraphQL is exactly:
👉 Here's a simple explanation
Structure matters: This is what our project looks like
Order is half the battle - and the code base is the whole battle. To ensure that our Flutter app remains maintainable and clear in the long term, we have developed our lib/data/graphql-structure is clearly organized:
lib/
└── data/
└── graphql/
├── __generated/ # Hier landen alle generierten dart-Dateien
├── fragments/ # Wiederverwendbare Snippets (.graphql)
├── mutations/ # GraphQL-Mutationen (.graphql)
├── queries/ # GraphQL-Queries (.graphql)
├── schema/
│ └── schema.graphql # Das API-Schema (via Symlink eingebunden)
├── subscriptions/ # Subscriptions, falls benötigt (.graphql)
├── graphql_service.dart # Setzt den GraphQLClient auf
└── index.dart. # Barrelfile für einfache ExporteFragments, mutations, queries and subscriptions are written directly as .graphql-files - neatly separated into the respective folders. This provides an overview and a clear separation between data logic and flutter code.
The central Scheme usually comes from the backend and lives outside the Flutter app. So that we can still work with it in Flutter, we bind it via Symlink into the project. We benefit directly from this:
- 🧠 Autocompletion when writing the
.graphql-files - 🛡️ Schema verification directly in the editor
- 🔄 Current status at all times - because we access the same schema as the backend team.
This is how you set the necessary symlink:
cd lib/data/graphql/schema
ln -s <relative-path-to-schema.graphql> ./schema.graphqlAlternatively, the schema can also be copied directly into the project - less elegant, but works just as well.
Important:
The folder __generated/ belongs to not in version control (Git). For this reason, it is consistently .gitignore The content is generated fresh with each build process.
# .gitignore
lib/data/graphql/__generated/Barrel files for simple imports (optional)
To avoid having to import each file individually, we set so-called Barrelfiles (e.g. index.dart). These export all the important contents of a folder:
// lib/data/graphql/queries/index.dart
export 'my-query-1.graphql.dart';
export 'my-query-2.graphql.dart';
...This allows us to import the entire GraphQL layer centrally - e.g. like this:
import 'package:myproject/data/graphql/index.dart';This reduces boilerplate and keeps our flutter components clean and compact.
The index.dart-Files do not have to be created manually in every folder - barrel files can be automated very easily 💡
I had ChatGPT write me a small dart script for this, which I use directly after the build_runner to ensure that the barrel files are always up to date. The script first removes all old index.dart-files and then generates new ones - including subdirectory handling:
import 'dart:io';
void main() {
final rootDir = Directory('lib/data/graphql');
if (!rootDir.existsSync()) {
print('Das Verzeichnis ${rootDir.path} existiert nicht.');
return;
}
print('Alte Barrel Files entfernen...');
deleteExistingBarrelFiles(rootDir);
print('Generiere neue Barrel Files für ${rootDir.path}...');
generateBarrelFiles(rootDir);
}
/// Löscht alle existierenden index.dart-Dateien im Verzeichnis und dessen Unterverzeichnissen
void deleteExistingBarrelFiles(Directory dir) {
for (var entity in dir.listSync(recursive: true)) {
if (entity is File && entity.path.endsWith('index.dart')) {
entity.deleteSync();
print('Gelöscht: ${entity.path}');
}
}
}
/// Generiert Barrel Files für ein Verzeichnis und seine Unterverzeichnisse
bool generateBarrelFiles(Directory dir) {
final dartFiles = <File>[];
final subDirs = <Directory>[];
final generatedSubDirIndexes = <Directory>[];
// Sammle alle .dart-Dateien und Unterverzeichnisse
for (var entity in dir.listSync(recursive: false)) {
if (entity is File &&
entity.path.endsWith('.dart') &&
!entity.path.endsWith('index.dart')) {
dartFiles.add(entity);
} else if (entity is Directory) {
subDirs.add(entity);
}
}
// Rekursiv für Unterverzeichnisse weitermachen und prüfen, ob index.dart erstellt wurde
for (var subDir in subDirs) {
if (generateBarrelFiles(subDir)) {
generatedSubDirIndexes.add(subDir);
}
}
// Barrel File für das aktuelle Verzeichnis erstellen
if (dartFiles.isNotEmpty || generatedSubDirIndexes.isNotEmpty) {
final barrelFile = File('${dir.path}/index.dart');
final buffer = StringBuffer();
// .dart-Dateien exportieren
for (var file in dartFiles) {
final filename = file.uri.pathSegments.last;
buffer.writeln("export '$filename';");
}
// Nur Unterverzeichnisse exportieren, deren index.dart erstellt wurde
for (var subDir in generatedSubDirIndexes) {
final subDirName =
subDir.uri.pathSegments[subDir.uri.pathSegments.length - 2];
buffer.writeln("export '$subDirName/index.dart';");
}
barrelFile.writeAsStringSync(buffer.toString());
print('Barrel File erstellt: ${barrelFile.path}');
return true;
}
return false;
}Simply as tools/generate_graphql_barrels.dart and execute it after code generation - e.g. with :
dart tools/generate_graphql_barrels.dartThis keeps everything clean and up to date - without any manual back and forth.
Developer Experience: Smooth thanks to tooling
To ensure that the integration of GraphQL in Flutter not only works, but is also fun, we rely on a setup that does as much as possible for us - from type safety to autocompletion.
The setup in four simple steps:
Install 1️⃣ packages
First we install the necessary packages:
flutter pub add graphql_flutter
flutter pub add --dev graphql_codegen build_runnergraphql_flutterprovides the GraphQL client, with all the trimmings.graphql_codegenandbuild_runnerprovide for the automatic generation of dart code from our.graphql-files.
Configure 2️⃣ build.yaml
In the build.yaml we define, where our GraphQL files are located and where the generated code is to be written. We also stipulate that we graphql_flutter as a client - so that queries and mutators are automatically generated in the correct format.
# build.yaml
targets:
$default:
builders:
graphql_codegen:
options:
clients:
- graphql_flutter
outputDirectory: /lib/data/graphql/__generated
assetsPath: lib/data/graphql/**.graphql3️⃣ Install VS Code Extension
👉 GraphQL: Language Feature Support
The extension brings us autocompletion, syntax highlighting and schema verification directly in the editor. A game changer, I promise 😎
4️⃣ create graphql.config.yaml
To link the whole thing together, we need a configuration file with a reference to the schema and our queries:
# graphql.config.yml
schema: 'lib/data/graphql/schema/schema.graphql'
documents: 'lib/**/*.{graphql,dart}'Our GraphQLService: A central interface to the backend
To ensure that our app communicates efficiently with the GraphQL API, we rely on a central GraphQLServicewhich encapsulates the entire communication. This gives us full control over authentication, error handling, logging, etc. - and we can access a preconfigured GraphQLClient access.
class GraphQLService {
final _authenticationService = locator<AuthenticationService>();
late GraphQLClient client;
GraphQLService() {
// Basis-HTTP-Link mit API-URL – zielt auf dein GraphQL-Backend
final httpLink = HttpLink(
AppEnvironment.apiBaseUrl,
);
// Authentifizierung – hier holen wir das Access Token aus unserem AuthService
// Du kannst hier auch eine andere Logik einbauen (z. B. Refresh Token, Whitelist etc.)
final authLink = AuthLink(
getToken: () async {
final accessToken = await _authenticationService.getAccessToken();
return accessToken != null ? 'Bearer $accessToken' : null;
},
);
// Die Link-Kette – hier kannst du auch Logging, Retry oder andere Middleware einhängen
final link = authLink.concat(httpLink);
client = GraphQLClient(
link: link,
// Cache-Strategie – hier verwenden wir einen InMemoryStore
// Für persistentes Caching könntest du z. B. HiveStore einsetzen
cache: GraphQLCache(store: InMemoryStore()),
// Timeout für Queries und Mutations – kann je nach Use Case angepasst werden
queryRequestTimeout: const Duration(seconds: 10));
// Du kannst dir hier zusätzliche Hilfsmethoden bauen, z. B. für typisierte Calls
// oder zentrale Fehlerbehandlung
}🎉 Have fun: Typed API calls - everywhere in the project
Whether in the Bloc, ViewModel or a other serviceThanks graphql_codegen and our central GraphQLService we access everywhere in our codebase typed to our API.
final result = await _graphQLService.client.query$MyQuery1();No more string fiddling, no more typos, no more forgotten fields - instead full autocompletion, type safety and a setup that simply feels good.
If you are working with Flutter and GraphQL want to get started - do it!
And if you have any questions or get stuck: We're happy to help 💜

Written by
Nando Schär





