Graphiti: a GraphQL schema builder that leaves your Swift types alone
The Swift GraphQL Schema builder for macOS and Linux
At a glance
- What is it?
- Graphiti is a Swift library for building GraphQL schemas from code, and its defining choice is that your entity types stay plain Codable structs with no protocol to conform to and no annotations to add. The schema is declared separately and tied to the entities through key paths and resolver functions, and the HTTP server is deliberately left to sibling packages.
- Who is it for?
- Adopt Graphiti if you are writing a GraphQL server in Swift and want a schema declared in code with real Swift types, dependency injection through the API type, and modular schema composition, and you are willing to depend on the GraphQLSwift ecosystem.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The design decision the README leads with
Graphiti is described as a Swift library for building GraphQL schemas fast, safely and easily in Swift code, and then the README immediately flags the choice it cares most about: one of the main design decisions behind Graphiti is not to pollute your entity declarations, so you can bring your entities to any other solution with ease.
That is the whole argument in one sentence, and it is unusual. Most GraphQL server libraries for statically typed languages work by making your types participate: a protocol to conform to, an annotation macro, a code generator, or a type registry. The cost is that the domain model now depends on the GraphQL layer, and untangling it later means rewriting the model.
Graphiti keeps the two separate. An entity is a plain struct, the schema is described in a separate place, and the two are joined by key paths and resolver functions. The entity can move to another framework, or to no framework, and the GraphQL layer is the only thing you rewrite.
The other half of the scope is stated just as plainly. Graphiti provides two important capabilities: building a type schema, and serving queries against that type schema. Building a schema is the library's job. The HTTP server is not: the README points you to graphql-vapor or graphql-hummingbird to expose the schema over HTTP, and to graphql-generator if you would rather generate Swift code from GraphQL SDL files instead of writing the schema by hand.
So the design space is: SDL in, generated code out is one project, HTTP serving is another two, and this one is the typed schema in the middle. That division is what keeps the package small enough to reason about, and it is also what you have to accept as an adopter.
Three plain Swift types: entity, context, resolver
The getting started sequence is deliberately small, and each step is free of Graphiti.
The entity is an ordinary Codable struct:
struct Message : Codable {
let content: String
}There is no protocol, no conformance, no annotation. Codable is there because GraphQL responses are serialised, not because the library demands it.
The context is where dependency injection happens, and the README is explicit that this step does not require Graphiti and is purely business logic. The context is passed to all of your field resolver functions, which means it is the place to put code that talks to a database or another service:
struct Context {
func message() -> Message {
Message(content: "Hello, world!")
}
}The resolver is the bridge, and it is the first type that imports Graphiti:
import Graphiti
struct Resolver {
func message(context: Context, arguments: NoArguments) -> Message {
context.message()
}
}The shape of that signature is the contract. A resolver method takes the context and the arguments, and returns a value. NoArguments is the type used when a field takes none, and there is a corresponding type per argument shape.
The point of the split is that the resolver is thin. It receives a context, calls business logic, and returns a result. Everything that a reader would want to test can be tested without a schema, and everything that a schema needs can be changed without touching the domain.
Resolvers can also be async, which is how a resolver that hits a database or a remote service looks:
struct Resolver {
func message(context: Context, arguments: NoArguments) async -> Message {
await someAsyncMethodToGetMessage()
}
}Since Graphiti supports Swift Concurrency, an async resolver is the normal case rather than a special one.
Declaring the schema: Type, Field and Query
The API type ties the resolver to the schema, and the schema is a Swift result builder expression.
struct MessageAPI : API {
let resolver: Resolver
let schema: Schema<Resolver, Context>
}
let api = MessageAPI(
resolver: Resolver()
schema: try! Schema<Resolver, Context> {
Type(Message.self) {
Field("content", at: \.content)
}
Query {
Field("message", at: Resolver.message)
}
}
)Read the two Field calls and the two roles they play. Inside Type(Message.self), the field is bound to a key path on the entity, \.content, which means the value is read straight off the model. Inside Query, the field is bound to a resolver method, Resolver.message, which means the value comes from your code.
That distinction is the mechanism by which the library avoids polluting entities. A field that is a property of the returned value needs no resolver at all, and a field that needs computation is attached to a method. The schema is where that decision is recorded.
The API struct exists so that dependency injection reaches the resolvers, and the README points out the practical consequence: you could pass mocks of resolver and context when testing. That is a small design choice with a large effect on testability, because the API is a value you construct, not a global you configure.
One rough edge is visible in the sample: the schema is built with try! in the README example. A schema with a type error therefore fails at construction rather than at request time, which is the right behaviour, but the example does not show how a caller is expected to handle the error in production code.
Modular schemas with SchemaBuilder and PartialSchema
A single result builder expression does not scale to a large schema, and Graphiti offers two ways to compose.
SchemaBuilder is the imperative form. You create a builder for the resolver and context types, add types and fields to it, and build:
let builder = SchemaBuilder(Resolver.self, Context.self)
builder.add(
Type(Message.self) {
Field("content", at: \.content)
}
)
builder.query.add(
Field("message", at: Resolver.message)
)
let schema = try builder.build()The important difference from the result builder is that the builder can be passed around and extended by other code, which is what makes it usable for a schema assembled from several modules rather than written in one place.
PartialSchema is the compositional form, and it comes in two shapes. One is a subclass with overridden type and query definitions:
final class ChatSchema: PartialSchema<Resolver, Context> {
@TypeDefinitions
public override var types: Types {
Type(Message.self) {
Field("content", at: \.content)
}
}
@FieldDefinitions
public override var query: Fields {
Field("message", at: Resolver.message)
}
}The other is a value form, PartialSchema.init with types and query closures, which is easier to construct dynamically. Both are then handed to the builder:
let schema = try SchemaBuilder(Resolver.self, Context.self)
.use(partials: [ChatSchema(), ...])
.build()The use method takes an array, and the trailing ... in the sample indicates that this is where the rest of your modules go. So the intended architecture is a set of partial schemas, one per feature area, combined by a builder, with the resolver and context types shared across all of them. That is a real answer to the question of how a large schema stays maintainable, and it is the part of the library that goes beyond a single-file example.
Executing a query and reading the result
Running a query is a single call on the API value:
let result = try await api.execute(
request: "{ message { content } }",
context: Context()
)
print(result)and the result is printed as:
{"data":{"message":{"content":"Hello, world!"}}}Two things are worth noticing. The request is a plain GraphQL query string, so Graphiti is a GraphQL implementation and not a query builder, and the context is passed at execution time rather than captured in the schema, which is what lets one schema serve requests with different contexts.
API.execute returns a GraphQLResult, which adopts Encodable, and the README notes that you can use it with a JSONEncoder to send the response back to the client using JSON. That detail is the hand-off point to the HTTP layer: a server package takes the result, encodes it, and writes it to the socket.
Subscriptions are supported through Swift Concurrency's AsyncThrowingStream, and the Usage Guide has a section on them. The library also keeps a path for people who cannot use Swift Concurrency: you must create a concrete subclass of the EventStream class that implements event streaming functionality, and if you would rather not write that subclass, the GraphQLRxSwift repository integrates RxSwift observables out of the box, and can serve as a reference for connecting a different stream library such as ReactiveSwift, OpenCombine, or one of your own.
For a feel for the whole thing, the README points at the Star Wars API in the test suite as a fairly complete example, and at the Usage Guide as a progressive walkthrough. It also points at the GraphQL specification README, and suggests working through that and the corresponding tests in parallel, with the examples in the specification existing as tests in the repository under Tests/GraphitiTests/StarWarsTests.
Where Graphiti stops and the sibling packages begin
The boundary between this library and the rest of the GraphQLSwift ecosystem is the thing to evaluate before you commit, because there is more than one valid way to assemble the stack.
Graphiti itself is the schema and execution layer. If you want HTTP, the README names two options, graphql-vapor for the Vapor web framework and graphql-hummingbird for the Hummingbird framework. If you would rather not hand-write the schema, graphql-generator produces Swift code from GraphQL SDL files, and that is a different starting point: you keep the SDL as the source of truth and regenerate the schema whenever it changes, rather than editing Swift directly.
The repository topics make the ecosystem visible, listing graphiti, graphql, swift, swift-nio, swiftpm and vapor, so Vapor is the framework the project is most closely associated with, and SwiftNIO is in the dependency chain.
The practical consequence of the split is that a Graphiti server is a multi-package dependency graph. The schema is one package, the HTTP layer is another, and the two have to be compatible with each other and with your Swift toolchain. If you already run a Vapor or Hummingbird application, that is an argument for Graphiti, because you are adding one package rather than a framework. If you do not, the dependency count is worth thinking about before you start, since the alternative of using a GraphQL server in a language whose package manager gives you one artifact is a real consideration.
A second limitation is worth stating plainly. Because entities are plain Swift types, Graphiti cannot generate a schema from a database model or from SDL, so a team that wants the database as the source of truth should use graphql-generator instead. And because there are no annotations, there is no way to express some of the things other libraries put on the type, such as custom scalar coercion or field deprecation metadata, so those have to be handled by whatever the HTTP layer or the result encoder provides.
Licence, releases and the version question
The licence is MIT, with the file at the root of the repository, so there is no copyleft obligation for a service built on it.
The releases are named after Italian wines, which is a small flourish and also a useful marker. The recent tags are 3.1.0, named Benasque, on 2026-02-07, 3.0.0, named Chianti, on 2025-08-21, and 2.1.0, named Verona, on 2025-06-09. So the project is on a 3.x line, and a 3.0.0 is a major version, which means there are breaking changes between the 2.x line and the current one, and the repository carries a MIGRATION.md file for that purpose.
The maintenance picture needs stating by date rather than by adjective. The last push to the repository was on 2026-04-06, and the most recent release, 3.1.0, is from 2026-02-07. That is a gap of several months between the newest release and the newest commit, so the tree is not receiving changes at the rate of the 2025 releases, and anyone evaluating should check the commit history rather than assume the pace continues.
There is one inconsistency in the README worth flagging, because it will send you to the wrong place. The getting started snippet still shows the package dependency pinned as:
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", .upToNextMinor(from: "0.20.1"))That is the 0.20 line, several major versions behind the current 3.1.0 tag. The snippet is stale, so take the version from the releases page rather than from the README, and read MIGRATION.md before jumping from a 2.x dependency.
On Swift versions, the README states the package supports Swift versions in alignment with Swift NIO, which is the standard way Swift server libraries handle a moving toolchain. The repository also carries a .swift-format configuration and a .codeclimate.yml, and the contributing instructions give the formatting command the project enforces:
swift format --parallel --in-place --recursive ./So a contribution is expected to be formatted with the standard tool before it is reviewed.
Editorial conclusion
Adopt Graphiti if you are writing a GraphQL server in Swift and want a schema declared in code with real Swift types, dependency injection through the API type, and modular schema composition, and you are willing to depend on the GraphQLSwift ecosystem. Do not adopt it if you need the HTTP layer in the same package, because serving over HTTP means adding graphql-vapor or graphql-hummingbird, and do not adopt it if your entities already carry a framework-specific GraphQL annotation scheme you intend to keep. Verify first which Swift version your toolchain is on, since the package aligns its supported versions with Swift NIO, that the release you pick matches the API you read in the README, because the getting started snippet still pins 0.20.1 while the latest tag is 3.1.0, and whether the 3.0.0 changes described in MIGRATION.md affect the code you are porting.
Frequently asked questions
What is Graphiti?
Graphiti is a Swift library for building GraphQL schemas in code, providing two capabilities: building a type schema and serving queries against that type schema. Its defining choice is that your entity types are not polluted with protocols or annotations, so they can be moved to another solution later.
How do I add Graphiti to a Swift package?
Add the package to Package.swift as a dependency with .package(url: "https://github.com/GraphQLSwift/Graphiti.git", .upToNextMinor(from: ...)), then import Graphiti. The README example still shows 0.20.1 in that snippet, so check the releases page for the current version before pinning.
How do I serve a Graphiti schema over HTTP?
Graphiti does not include an HTTP server. The README points to graphql-vapor for the Vapor framework and graphql-hummingbird for Hummingbird, and to graphql-generator if you want Swift code generated from GraphQL SDL files instead of writing the schema by hand.
Does Graphiti support subscriptions and async resolvers?
Yes. Resolver functions can be async, and subscriptions are supported through the Swift Concurrency AsyncThrowingStream type, with details in the Usage Guide. If you cannot use Swift Concurrency, you must subclass EventStream yourself, or use the GraphQLRxSwift repository for RxSwift support.
Can a Graphiti schema be split into modules?
Yes. SchemaBuilder lets you add types and fields imperatively and then build the schema, and PartialSchema provides both a subclass form with overridden type and query definitions and a value form, so a large schema can be assembled from one partial per feature area with the use(partials:) method.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/graphqlswift-graphiti)Was this useful?