GraphQL subscriptions at scale with NATS
In this article, we'll look at how to setup GraphQL subscriptions at scale with NATS.
Note: If you're not familiar with NATS, please checkout my earlier article.
Why GraphQL subscriptions?
In my opinion, subscriptions are quite underrated and often overlooked. They provide just the right amount of abscraction over websockets, which is both developer friendly and powerful. Plus all the tooling around GraphQL is simply fantastic, from ease of integration to code generators, it's the perfect choice to reduce complexity on the frontend.
What and How?
Here, we'll create a simple GraphQL server and subscribe to a subject from our resolver. We'll use GraphQL playground to mock client side behavior. Once we're connected we'll use NATS CLI to send a payload to our subject and see the changes on the client.
Note: NATS client is available in over 40 different languages.
Pre-requisites
We will require following tools to run our example, so make sure you have these installed first.
Setup
Let's start by setting up a basic GraphQL project using gqlgen which lets us autogenerate our schema, resolvers and much more.
Note: All the code from this article will be available in this repo
Init a new go module.
$ mkdir example
$ cd example
$ go mod init example
Create a new gqlgen project.
$ printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go
$ go mod tidy
$ go run github.com/99designs/gqlgen init
$ printf 'package model' | gofmt > graph/model/doc.go
This should create the following directory structure
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│ ├── generated
│ │ └── generated.go
│ ├── model
│ │ ├── doc.go
│ │ └── models_gen.go
│ ├── resolver.go
│ ├── schema.graphqls
│ └── schema.resolvers.go
├── server.go
└── tools.go
Before we run this, let's simplify the graph/schema.graphqls
as below to keep things as minimal as possible so we can focus on NATS integration more clearly.
type Subscription {
payload: String
}
type Query {
payload: String
}
Let's re-generate our resolvers
$ go run github.com/99designs/gqlgen generate
validation failed: packages.Load: nats-gql/graph/schema.resolvers.go:36:72: NewTodo not declared by package model
nats-gql/graph/schema.resolvers.go:36:89: Todo not declared by package model
nats-gql/graph/schema.resolvers.go:39:62: Todo not declared by package model
nats-gql/graph/schema.resolvers.go:42:41: MutationResolver not declared by package generated
exit status 1
This should give an error, which tells us to remove unused code from our resolvers. To fix this simply open graph/schema.resolvers.go
and remove anything below the // !!! WARNING !!!
sign.
Now, let's also change the query resolver Payload
.
func (r *queryResolver) Payload(ctx context.Context) (*string, error) {
value := "hello world"
return &value, nil
}
And finally let's run the server!
$ go run server.go
2022/02/15 18:20:56 connect to http://localhost:8080/ for GraphQL playground
Wohoo! Now we can go to localhost:8080
and run our sample query in GraphQL playground and it should give us a result.
Now that we have our basic GraphQL server working, let's look into our subscription resolver defined in schema.resolvers.go
.
But first, let's understand the Payload
resolver function. As we know, Go has this amazing concept of channels and if we look at the return signature this function requires us to return a receive only channel of type *string
along with error
in case something goes wrong. This seems quite idiomatic Go to me!
Note: gqlgen will forward any data sent to this channel to the subcription.
func (r *subscriptionResolver) Payload(ctx context.Context) (<-chan *string, error)
Note: Notice that *string
will be changed if we update our schema and regenerate our resolvers
This is perfect! Let's install nats.go
.
$ go get github.com/nats-io/nats.go
gqlgen is setup in such a way that it makes it very easy to do dependency injection. So, let's init our NATS client. But first let's update some types.
In graph/resolver.go
update the Resolver
type as below.
type Resolver struct{
Nats *nats.Conn
}
Nice! now we'll be able to init and pass our client to graph.Resolver
struct in server.go
.
nc, err := nats.Connect(nats.DefaultURL)
if err != nil {
panic(err)
}
defer nc.Close()
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{nc}}))
Great! moving back to graph/schema_resolvers.go
let's define a channel and subscribe to a subject like payload-subject
. In the callback function, we'll simply convert our message data which is a byte array to string and send it to our channel.
func (r *subscriptionResolver) Payload(ctx context.Context) (<-chan *string, error) {
ch := make(chan *string)
r.Nats.Subscribe("payload-subject", func(msg *nats.Msg) {
payload := string(msg.Data)
ch <- &payload
})
return ch, nil
}
Note: You can easily setup an encoded connection to auto-parse any json data. NATS makes it super convenient!
Before we run our app, let's open a new terminal window and start our NATS server.
$ nats-server
[17275] 2022/02/15 18:57:30.517959 [INF] Starting nats-server
[17275] 2022/02/15 18:57:30.518427 [INF] Version: 2.7.0
[17275] 2022/02/15 18:57:30.518431 [INF] Git: [not set]
[17275] 2022/02/15 18:57:30.518439 [INF] Name: NDR3HVHJHVJKXIAIXYZWLUEYTEG6MRSOSCHLW2QXEKA2GSZ2JKBTI3DA
[17275] 2022/02/15 18:57:30.518442 [INF] ID: NDR3HVHJHVJKXIAIXYZWLUEYTEG6MRSOSCHLW2QXEKA2GSZ2JKBTI3DA
[17275] 2022/02/15 18:57:30.521185 [INF] Listening for client connections on 0.0.0.0:4222
[17275] 2022/02/15 18:57:30.521621 [INF] Server is read
Note: Want to run NATS in production? Checkout my ealier article where we look at different ways of running a NATS server on Kubernetes.
Finally! Let's start our app and navigate to localhost:8080
$ go run server.go
2022/02/15 19:03:22 connect to http://localhost:8080/ for GraphQL playground
Let's start a subscription for a query.
subscription {
payload
}
Now it should be actively listening for changes. Lastly, let's publish a message, usually this will be performed by a another service or client but right now we can just use the nats cli. i.e nats pub <subject> <payload>
$ nats pub payload-subject "hello world"
Conclusion
Perfect! Now we can run our real time GraphQL subscriptions at scale, all thanks to NATS which is capable of serving upto 18 million messages per second (yes, you read that right!). I hope this article was helpful, as always if you have any questions feel free to reachout to me on LinkedIn or Twitter.
Top comments (0)