Development Blog

Where we pretend to know how to code.

New Web (API) Foundations (Part Four)

Published: 2021-10-20

Author: Teddi

At the time I had already written a few reasonable applications in Golang which had all leaned into the strengths of the language. One strength is that Golang is strongly typed, so strongly typed in fact that generics (or the any type to you TSers) don’t exist in Go yet. Even though on average I’m writing 20-35% more code it’s never really felt like an issue because the on-save checks generally catch 99% of my mistakes with compilation catching the last 1% and besides, with tiny binaries with fast execution it’s always been bit of a dream.

As a result of these really positive experiences this all felt like we were very much on track and that this would probably be lightning fast. After all, if I can do this and it works so easily then this must be the future… right?

Well building this API turned out to have a couple of curveballs, though none of them were necessarily the fault of the language.

For the initial implementation of the API I decided to implement three key routes to test out gqlgen, these being:

  • Player details
  • Item metadata
  • Title metadata

The reason behind these is each one of them loosely touched upon key benefits that graphql can give us.

  • Player details: We have both “core” and “optional” details.
    • Core data could be considered Cubes, SteamID and so on.
    • Optional details are more like loading a players inventory, their achievements etc.
  • Item metadata: We can have optional parameters which impact the data we get back.
    • Specify an item ID? You only get that item. Don’t specify? All items.
    • We’re guaranteed to return everything even if you don’t ask for it because it’s part and parcel
  • Title metadata: This was actually going to have some upcoming changes so being able to set it now and then in theory update it without any worry to the recipient proved to be an interesting experiment.
    • Also in theory other than the ID, all returned arguments are optional.

As gqlgen is all codegen and you just “provide” your schema; for the Player this looks something like the following:

type Player {
  id: Int!
  steamID64: String!
  name: String!
  cubes: Int
  title: String
  titles: [Int!]
  timeSpent: Int
  lastPlayed: Int
  lastServer: String
  firstJoined: Int
  isPlatinum: Boolean
  achievements: [Achievement]
  discordSnowflake: Int
  inventory: [Item]

And likewise the actual “query” object looks something like this:

type Query {
  player(account_id: Int, steamID64: String): Player!

For the sake of clarity anything with a ! in it is required for a parameter. If it’s omitted it’s optional. So requesting a Player object takes either an account ID or a steamID64. If neither are present we throw back an error.

At this point we’re now pivoting, just when it was getting good!

In theory at this point all I had to do was run go generate ./... (from the base directory) and everything would be codegenned, types would be made and we’d be in business. That wasn’t the case here.

Although it’s now resolved apparently some libraries had gotten very much out of sync that gqlgen used meaning the latest release of it was actually entirely broken and typically when you pull libraries for the first time to learn them you don’t expect this to be the case, so I lost around 3 hours or so trying to debug this (with some furious googling amongst other things). Eventually after seeing a few reported issues that suggested the latest few releases were a wee bit broken rolling back to an older version resolved that.

But our woes weren’t entirely over yet. Due to how gqlgen does library / package resolving with types in the library it means that each import is in theory a fully qualified library in Go. So an import in Go for all of our codegen looks like "" which isn’t exactly correct because at the time of writing it’s trying to reference which doesn’t exist! And even if it did, what about future code? This makes no sense!

The good news is this is a problem Go has already solved, especially if you’re using module mode (which since 1.16 you should be!). After a bit of research it was thankfully as easy as doing replace => ./src/generated in the go.mod file (which is a bit like a schema / packages / requirements file) and Go knows immediately where to look instead.

By far not deal breakers but absolutely things that can trip up even a seasoned developer.

When things were eventually working it was quite interesting to see what got generated! While I won’t post all of it (as it’s a whole lot) here’s an example of the “basic” output of the Player object:

type Player struct {
	ID               int            `json:"id"`
	SteamID64        string         `json:"steamID64"`
	Name             string         `json:"name"`
	Cubes            *int           `json:"cubes"`
	Title            *string        `json:"title"`
	Titles           []int          `json:"titles"`
	TimeSpent        *int           `json:"timeSpent"`
	LastPlayed       *int           `json:"lastPlayed"`
	LastServer       *string        `json:"lastServer"`
	FirstJoined      *int           `json:"firstJoined"`
	IsPlatinum       *bool          `json:"isPlatinum"`
	Achievements     []*Achievement `json:"achievements"`
	DiscordSnowflake *int           `json:"discordSnowflake"`
	Inventory        []*Item        `json:"inventory"`

And the corresponding receiver function that requests “hit”:

func (r *queryResolver) Player(ctx context.Context, accountID *int, steamID64 *string) (*model.Player, error) {
	// TODO: Fill me out
	return nil, nil

At first glance it seems rudimentary, even basic! There’s some pretty clever nuances going on here.

  • Firstly gqlgen tags each struct entry with the corresponding JSON flag to make marshalling / unmarshalling JSON super easy. While standard practise is to do this with known types, the fact it does it for you is nice.
  • Secondly, the accountID and steamID64 arguments are both pointers. Golang doesn’t do optional arguments but a pointer in an argument can be nil meaning if it doesn’t exist, you can treat it in a safe manner.
  • Finally: If you have any custom objects or methods (e.g. a database connection) queryResolver can hold it for you, meaning you can have per-request data and no worries about global states floating about.

Already we have a nice chunk of work done for us! So when filled out you’d have something like the following (use your imagination a bit here):

func (r *queryResolver) Player(ctx context.Context, accountID *int, steamID64 *string) (*model.Player, error) {
	var playerDetails model.Player

	// We query for stuff and all stuff it in to the playerDetails object..

	return &playerDetails, nil

gqlgen then handles converting all that into a json response and serving it to the player. Once I filled in the first three entries (Players / Item meta data / title meta data) I had a pretty strong handle on the system; everything is nicely typed, returns what it says on the tin and there’s no weird interface{} magic going on. To give an idea of how easy this was, each endpoint in 90% of cases was no more than a few minutes worth of work to get implemented with the exception being the data for surf ranks. Nothing special as to why: just wanted to try enums in graphql and just needed a few bits of logic to handle different leaderboard types.

When I had originally set up the Python API, it had taken me a reasonable amount of time to test, validate, be happy with the end results, handcraft a load of things and so on. gqlgen meant I literally had the entire basic system up and running in a night! Any additions since then literally take a few minutes to add and validate their correctness with sometimes a bit of extra time if the endpoint has to be “protected”.

At this point I compiled it and threw it online for testing for myself and Killermon. While this API wasn’t yet finished (I was determined for authentication + authorization to be done first) I wanted to make sure that this could be placed online without too much effort and that it would work.

It simply worked. I have enough practise and history with nginx that this sort of thing is a breeze and yeah, it was immediately queriable!

However we absolutely needed authentication and authorization to be handled. This wouldn’t be a success without those and I was utterly determined to make sure that was sorted and sorted properly. That said - the way I had gone about this, was there even a sane way to do that?

Tune in next time for that part!

Historical Posts