This note is a high-level personal reminder for key steps to build a GraphQL API in Go with a SQLite database. The goal was to take scraped JSON data, migrate it to a database, and expose it through a GraphQL API using a schema-first approach with gqlgen.

Repo with all the code: address-api

  1. Initial Project Structure πŸ—οΈ

Organize the repo however, but I went with a typical enterprise structure because I wanted to understand this structure better. For this small project, all the code could have been in just a couple of files.

β”œβ”€β”€ cmd               # Main application entry points
β”‚   β”œβ”€β”€ server        # For the long-running API server
β”‚   └── setup         # For one-time setup scripts (like DB migration)
β”œβ”€β”€ db                # Database-related files
β”‚   └── migrations    # SQL files for schema changes
β”œβ”€β”€ internal          # Private application logic
β”‚   └── repository    # Data access layer
└── graph             # GraphQL generated code and resolvers
  1. Database Migration 🚚

Move data from its source (e.g., JSON files) into your database.

  • Create a Schema: Define your table structure in an .sql file (e.g., db/migrations/001_create_tables.sql).
  • Write a Go Script: Create a one-time script (cmd/setup/main.go) that reads the source data, connects to the SQLite DB, creates the tables, and runs INSERT statements to populate it.
  • Run the Migration: Execute the script before starting your server: go run ./cmd/setup.
Note

The address-api repo also has a one-time script file, internal/data/cleanup.go, that cleans up each JSON file. I ran it once, hence it does not have tests associated with it. Also, keygen.go is a file that generates random 32byte API key.

  1. GraphQL Schema-First Design πŸ“

Define your API contract first, then generate the Go boilerplate.

  • Initialize gqlgen:
go run [github.com/99designs/gqlgen](https://github.com/99designs/gqlgen) init
  • Define Your Schema: Edit graph/schema.graphqls to define your types, querys, and directives. Use camelCase for field names.
  • Generate Go Code: Run the generate command to create Go models and resolver stubs.
 go run [github.com/99designs/gqlgen](https://github.com/99designs/gqlgen) generate
Tip

Define the generate command in a Makefile. When you add a mutation or query in the future, you can simply run make generate.

  1. Refactor to the Repository Pattern πŸ›οΈ

To keep the code clean and testable, we separate the database logic (the β€œChef”) from the API logic (the β€œWaiter”).

  • Define an Interface: In the internal/repository package, create an interface that defines the data access methods (e.g., GetCountryCode). This is the β€œcontract.”
  • Create a Concrete Repository: Create a struct (e.g., AddressRepository) with a *sql.DB field. Implement the interface methods on this struct. This is where all your SQL queries live.
  • Inject the Repository: Update the main graph.Resolver struct to hold the repository interface, not the *sql.DB directly.
  1. Implement Resolvers & Directives πŸ”Œ

With the repository in place, the resolver’s job becomes very simple.

Update main.go:

  • Create the DB connection.
  • Create an instance of the repository (e.g., repo := repository.NewAddressRepository(db)).
  • Create the main resolver, injecting the repository (resolver := &graph.Resolver{Repo: repo}).

Write Resolver Logic: In graph/schema.resolvers.go, the resolver functions become simple one-liners that just call the corresponding method on the repository.

func (r *queryResolver) CountryCodes(ctx context.Context, country *string) ([]*model.CountryInfo, error) {
        return r.Repo.GetCountryCode(ctx, country)
    }

Implement Directives: For features like auth, implement the directive function that was generated by gqlgen.

  1. Add Authentication πŸ”

I went with API key approach, which is simple.

  • Protect specific queries so they require an API key.
  • Update Schema: Add a directive definition (directive @auth on FIELD_DEFINITION) and apply it to a query (myQuery: String! @auth).
  • Create HTTP Middleware: Create a middleware that reads the Authorization:
  • Bearer header and injects the token into the request context. Also, Implement Directive: The Auth directive function reads the token from the context. If the token is valid, it calls next(ctx) to proceed; otherwise, it returns an error.

Finally, write tests and deploy. 😊