Skip to content

Generate a Typed GraphQL Client for Dart in One Command

A GraphQL schema is fully typed. Then you write the Dart client by hand, and the types stop at the network boundary: Map<String, dynamic>, string keys, casts, and a query string you hope matches the server.

GraphLink skips that part. It reads your schema and writes the client for you, with models, inputs, queries and mutations, all typed and null-safe. This post is the quickstart: one schema, one command, and a first query against a real server. The video shows all of it, and the sections below follow the same order.

Watch on YouTube

The schema

The example is a small bookstore: authors and books, two queries and one mutation. It is deliberately simple.

type Author {
  id: ID!
  name: String!
}

type Book {
  id: ID!
  title: String!
  year: Int!
  author: Author!
}

input AddBookInput {
  title: String!
  year: Int!
  authorId: ID!
}

type Query {
  book(id: ID!): Book
  books: [Book!]!
}

type Mutation {
  addBook(input: AddBookInput!): Book!
}

One config file, one command

GraphLink needs a small config file that points at the schema and says where the output goes:

{
  "schemaPaths": ["schema.graphql"],
  "mode": "client",
  "typeMappings": {
    "ID": "String",
    "String": "String",
    "Float": "double",
    "Int": "int",
    "Boolean": "bool"
  },
  "outputDir": "lib/generated",
  "clientConfig": {
    "dart": {
      "packageName": "bookstore_quickstart"
    }
  }
}

packageName has to match the name in your pubspec.yaml. Then you run the CLI:

glink -c config.json

In the video it reports 30 files generated in about 10 milliseconds. Installation instructions for glink are in the docs at graphlink.dev.

What you get

Under lib/generated there are a few folders (client, enums, inputs, interfaces and types) and one file, graphlink.dart, which is the only thing you import.

Take the input type from the schema. The generated Dart class has typed fields, a constructor with named parameters, and toJson and fromJson. That means you don't need json_serializable, and you can remove it from your dependencies.

It is null-safe because the schema is. If you make a field optional in the schema, for example name on Author, run glink again and the Dart field and the constructor parameter become optional too. The compiler then points at every place in your code that needs to change.

Look at pubspec.yaml and you will find only http. GraphLink is not a dependency. It is a code generator, so the output is plain Dart, and if you delete GraphLink tomorrow your code still compiles.

The adapter and the first query

The generated client needs an adapter. An adapter is a function that takes the payload and returns a Future<String> with the server's response. That is the only way GraphLink talks to your server, so it doesn't care how the data travels. For HTTP, GraphLink generates one for you:

final client = GraphLinkClient(
  adapter: GraphLinkHttpAdapter(url: 'http://localhost:8080/graphql'),
);

final response = await client.queries.books();
for (final book in response.books) {
  print('${book.title} (${book.year}) by ${book.author.name}');
}

The query for book(id: ID!) has a required, named id parameter, and book.title and book.author.name are typed values, not map lookups. In the video the server is a small Spring Boot app, and fetching book 1 and then book 2 returns the right titles and IDs.

No magic

The last part of the video opens the generated query. The client declares the query and a map of fragments, one "all fields" fragment per type, and sends only the fragments the query needs. For Book that means its own fields plus the fragment for Author. All of it is ordinary Dart that you can read.

Try it

The rest of the Dart series covers custom queries, fragments, batching, caching and subscriptions.