Open Source · MIT · v4.2.0

Your GraphQL Schema
Is Already the Contract.

GraphLink reads your schema and generates fully typed client and server code for Dart / Flutter and Java / Spring Boot. No boilerplate. No generics. No drift between schema and code.

In production: 72% of Spring Boot files & 21.5% of a full Flutter codebase are generated — zero hand-written boilerplate.

schema.graphql
type Query {
  getUser(id: ID!): User!
    @glCache(ttl: 300, tags: ["users"])
}

type Mutation {
  updateUser(
    id: ID!, input: UserInput!
  ): User! @glCacheInvalidate(tags: ["users"])
}

type User {
  id: ID!
  name: String!
  email: String!
}
glink generate
// Fully typed. Zero boilerplate.
final res = await client.queries
    .getUser(id: '42');

// res.getUser is a typed User — no casting
print(res.getUser.name);
// No generics. No casting. Just types.
GetUserResponse res =
    client.queries.getUser("42");

// res.getUser() returns a typed User
System.out.println(
    res.getUser().getName());

Measured in production — multi-tenant SaaS, dialysis clinic management platform

72% of Spring Boot files are generated
64% of Spring Boot lines are generated
21.5% of Flutter codebase is generated
135 hand-written files in a full Spring Boot app

Only 135 files (~11.8k lines) were written by hand in the entire Spring Boot backend. GraphLink wrote the rest — controllers, service interfaces, DTOs, input classes, repositories, and batch mappings. Every generated file compiles, ships, and runs with zero runtime dependency on GraphLink.

The GraphQL Tax Nobody Talks About

GraphQL is great for describing your API. But between the schema and your first real call, there is a wall of repetitive, error-prone code that has nothing to do with your business logic.

⚠️

Duplicate type definitions

You define User in your schema, then again as a Java class, then again as a Dart model. Three places. Three chances to drift.

💥

Schema drift in production

A field gets renamed on the backend. The app crashes. The client model was never updated. The schema was the truth — nobody told the code.

😤

Generic soup in Java

Some GraphQL clients force you to pass TypeReference<Response<GetUserData>> every single call. For every single query.

🔁

Adding a field = 5 file edits

Schema, data class, fromJson, toJson, query string. Every new field is a five-stop tour of your codebase — and one missed stop breaks everything.

⏱️

Serialization by hand, forever

Writing fromJson / toJson for the 50th time this year. You know you should automate this. You haven't gotten around to it.

🔌

WebSocket subscriptions are a project

Connecting, handshaking, reconnecting, parsing frames. A full afternoon for something that should be one method call.

Define Once. Generate Everything.

GraphLink turns your schema into production-ready code in seconds. The output is readable, idiomatic, and has no runtime dependency on GraphLink.

01

Write your GraphQL schema

Define your types, queries, mutations, subscriptions. Add @glCache where you want built-in caching. The schema is your only source of truth.

02

Run one command

Run glink -c config.json. Point it at your schema, pick your target language, and GraphLink writes all the files. That is the entire workflow.

03

Call fully-typed, generated code

Your IDE sees the generated types immediately. Queries are methods. Responses are typed objects. Subscriptions are streams. No wiring needed.

What a medium-complexity schema generates:

types/ User.dart Post.dart Comment.dart
inputs/ CreatePostInput.dart UpdateUserInput.dart
enums/ UserRole.dart PostStatus.dart
client/ GraphLinkClient.dart

All generated files are yours. No hidden runtime. Stop using GraphLink any time — the files keep working.

See the Difference

A single query on a User type with three fields. Multiply this by every query in your app — then you understand why 64% of a real Spring Boot codebase is generated.

Without GraphLink ~65 lines
// 1. Write the query string manually
String GET_USER = """
    query GetUser($id: ID!) {
      getUser(id: $id) { id name email }
    }
""";

// 2. Write the data class manually
public class User {
    private String id;
    private String name;
    private String email;

    public String getId()   { return id; }
    public void setId(String id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String n) { this.name = n; }
    public String getEmail() { return email; }
    public void setEmail(String e) { this.email = e; }
}

// 3. Write the response wrapper manually
public class GetUserData {
    private User getUser;
    public User getGetUser() { return getUser; }
    public void setGetUser(User u) { this.getUser = u; }
}

// 4. Execute — with generics
GraphQLResponse<GetUserData> response = client.execute(
    GET_USER,
    Map.of("id", userId),
    new TypeReference<GraphQLResponse<GetUserData>>() {}
);
User user = response.getData().getGetUser();
With GraphLink 0 written · ~120 generated
// That's it.
User user = client.queries
    .getUser(userId)
    .getUser();
No generics. No TypeReference. No casting.
Just a method call that returns exactly the type you expect.
Without GraphLink ~50 lines
// 1. Write the data class manually
class User {
  final String id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'] as String,
    name: json['name'] as String,
    email: json['email'] as String,
  );

  Map<String, dynamic> toJson() => {
    'id': id, 'name': name, 'email': email,
  };
}

// 2. Write the query string manually
const getUserQuery = r'''
  query GetUser($id: ID!) {
    getUser(id: $id) { id name email }
  }
''';

// 3. Write the HTTP call and parse manually
final resp = await http.post(
  Uri.parse(graphqlUrl),
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({
    'query': getUserQuery,
    'variables': {'id': userId},
  }),
);
final data = jsonDecode(resp.body);
final user = User.fromJson(
  data['data']['getUser'] as Map<String, dynamic>,
);
With GraphLink 0 written · ~80 generated
// That's it.
final res = await client.queries
    .getUser(id: userId);

final user = res.getUser; // typed User
No fromJson. No query strings. No HTTP wiring.
GraphLink generates and maintains all of it, forever in sync with your schema.

More Than You Expected

🎯

Type-Safe Client

Every query and mutation becomes a typed method call. Arguments are named parameters. Responses are typed classes. Your IDE autocompletes everything.

⚙️

Full Server Generation

For Spring Boot: generates controllers, service interfaces, and repository stubs. Not skeleton stubs — real, compilable code ready for your business logic.

💾

Built-In Caching

Cache behavior lives in the schema via two directives — not scattered across your client code. Tag-based invalidation. Partial query caching. Offline fallback.

See how it works →
👁️

Watch Mode

Run with -w and GraphLink watches your schema files. Every save triggers instant regeneration. Your IDE sees the updated types without restarting.

🔌

WebSocket Subscriptions

Subscriptions are generated alongside queries and mutations. Connection, handshake, message parsing, reconnection — all handled in the generated client.

🔓

Zero Lock-In

Generated code has no runtime dependency on GraphLink. Stop using it any time — the files keep compiling and working. You own every line of the output.

Cache Control Lives in the Schema

Most GraphQL clients require you to configure caching in application code — separate from the schema, spread across components. GraphLink puts it exactly where the data contract lives.

schema.graphql
type Query {
  # Cached 5 min, tagged "cars"
  getCar(id: ID!): Car!
    @glCache(ttl: 300, tags: ["cars"])

  # Each field cached independently
  getCarAndOwner(
    carId: ID!, ownerId: ID!
  ): CarAndOwner! @glCache(ttl: 60) {
    car: getCar(id: $carId)
      @glCache(ttl: 300, tags: ["cars"])
    owner: getOwner(id: $ownerId)
      @glCache(ttl: 120, tags: ["owners"])
  }
}

type Mutation {
  # Busts all "cars" entries on success
  createCar(input: CreateCarInput!): Car!
    @glCacheInvalidate(tags: ["cars"])
}

ttl — Time to live

Entries expire after this many seconds. First call hits the network. Subsequent calls are served from cache with no extra code.

🏷

tags — Targeted invalidation

Tag related entries together. A single mutation can evict an entire group. Unrelated cache entries are untouched and stay warm.

🧩

Partial query caching

Each field in a compound query has its own TTL. GraphLink fetches only what is missing or expired. If only "cars" is invalidated, "owners" is still served from cache.

📴

staleIfOffline — Offline resilience

Return the expired entry when the network is unavailable instead of throwing. One flag in the schema. Zero extra code in your app.

For Java developers

No Generics at the Call Site. Ever.

You've used Java GraphQL clients that require a TypeReference<GraphQLResponse<GetUserData>> on every single call. GraphLink generates fully-resolved return types. The call site reads like ordinary Java code — because it is.

Other clients
GraphQLResponse<GetUserData> res = client.execute(
    GetUserQuery.builder().id("42").build(),
    new TypeReference<GraphQLResponse<GetUserData>>() {}
);
User user = res.getData().getGetUser();
GraphLink
User user = client.queries
    .getUser("42").getUser();

Up in 5 Minutes

1

Download the CLI binary

# Download for your platform from GitHub Releases:
# github.com/Oualitsen/graphlink/releases/latest
#
# Linux:   glink-linux-x86_64
# macOS:   glink-macos-arm64
# Windows: glink-windows-x86_64.exe

chmod +x glink-linux-x86_64
mv glink-linux-x86_64 /usr/local/bin/glink
2

Create config.json

{
  "schemaPaths": ["lib/**/*.graphql"],
  "mode": "client",
  "typeMappings": {
    "ID": "String",
    "Float": "double",
    "Int": "int",
    "Boolean": "bool"
  },
  "outputDir": "lib/generated",
  "clientConfig": {
    "dart": {
      "packageName": "my_app",
      "generateAllFieldsFragments": true,
      "autoGenerateQueries": true
    }
  }
}
3

Generate

glink -c config.json

# Or watch mode — regenerates on every schema save:
glink -c config.json -w
1

Download the CLI binary

# Download for your platform from GitHub Releases:
# github.com/Oualitsen/graphlink/releases/latest
#
# Linux:   glink-linux-x86_64
# macOS:   glink-macos-arm64
# Windows: glink-windows-x86_64.exe

chmod +x glink-linux-x86_64
mv glink-linux-x86_64 /usr/local/bin/glink
2

Create config.json

{
  "schemaPaths": ["schema/*.gql"],
  "mode": "client",
  "typeMappings": {
    "ID": "String",
    "Float": "Double",
    "Int": "Integer",
    "Boolean": "Boolean"
  },
  "outputDir": "src/main/java/com/example/generated",
  "clientConfig": {
    "java": {
      "packageName": "com.example.generated",
      "generateAllFieldsFragments": true,
      "autoGenerateQueries": true
    }
  }
}
3

Generate

glink -c config.json

# Or watch mode — regenerates on every schema save:
glink -c config.json -w

Growing With the Ecosystem

GraphLink is actively developed. The next major target is TypeScript — bringing the same zero-boilerplate experience to JavaScript/TypeScript frontends and Node.js backends. Go and Kotlin targets will follow based on community demand.

✓ Available

Dart / Flutter Client

Full client with queries, mutations, subscriptions, and built-in caching.

✓ Available

Java Client

Type-safe client with no generics at the call site and builder-pattern inputs.

✓ Available

Spring Boot Server

Controllers, service interfaces, and repository stubs generated from schema.

🔧 In Development

TypeScript Client

Type-safe client for React, Vue, Angular, and Node.js. Same zero-boilerplate philosophy.

📋 Planned

Express / Node.js Server

Server-side code generation for Node.js GraphQL backends.

📋 On Demand

Go & Kotlin

Additional targets based on community demand. Star the repo to show interest.

Need a language or framework not on the list?

Request a target →

Frequently Asked Questions

Does the generated code have a runtime dependency on GraphLink?

No. The generated code has zero runtime dependency on GraphLink. If you stop using GraphLink tomorrow, every generated file continues to compile and work exactly as before. You own the output completely — it is ordinary Dart or Java code.

Does the Java client use generics at the call site?

Never. GraphLink generates fully-resolved return types for every query and mutation. The call site is just client.queries.getUser(id).getUser() — no TypeReference, no casting, no generic parameters. Other Java GraphQL clients often force you to pass new TypeReference<GraphQLResponse<GetUserData>>(){} on every single call. GraphLink does not.

What happens when I add a field to the schema?

Run glink -c config.json again (or let watch mode pick it up automatically). All affected files are regenerated and your new field is immediately available as a typed property. You edit one file — the schema — and GraphLink handles the rest.

How does the built-in caching work?

Cache behavior is declared directly in the schema using two directives. @glCache(ttl: 300, tags: ["cars"]) caches a query result for 300 seconds under the tag "cars". @glCacheInvalidate(tags: ["cars"]) on a mutation evicts all entries tagged "cars" when the mutation succeeds. You can also apply @glCache to individual fields inside a compound query so each field is cached independently with its own TTL — if one tag is invalidated, the others stay warm.

What languages and frameworks are supported?

Currently: Dart and Flutter (client), Java (client), and Spring Boot (server — controllers, services, repositories). TypeScript support is actively in development. Go and Kotlin targets are planned based on community demand.

Is GraphLink production-ready?

Yes. GraphLink is used in production in a large multi-tenant SaaS platform (dialysis clinic management). In that project, 72% of Spring Boot files and 64% of lines are generated — only 135 files (~11.8k lines) were written by hand across the entire backend. On the Flutter side, 21.5% of the codebase is generated, covering all DTOs, input classes, enums, and GraphQL client wiring.

The generated code is idiomatic, readable, and fully debuggable — it looks exactly like code you would have written by hand. There is no runtime magic, no hidden abstraction layer, and no dependency on GraphLink at runtime.

Can I use GraphLink with an existing project?

Yes. Point schemaPaths in your config.json at your existing .graphql files and set outputDir to wherever you want the generated files. GraphLink does not modify any of your existing source files.