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
- 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
- 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.
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.
- 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
Define the generate command in a Makefile. When you add a mutation or query in the future, you can simply run make generate.
- 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.
- 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.
- 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. π