Published on

GraphQL subscriptions at scale with NATS

6 min read

Authors
banner

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.

architecture

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. query

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"

nats-subscription

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.

© 2024 Karan Pratap Singh