Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
08 Jul 2025 · 5 min read ·Article 8 / 125
Go

8 Ideal Directory Structures for a graphql-go Project

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

8 Ideal Directory Structures for a graphql-go Project

GraphQL has become a mainstream solution for APIs thanks to its flexible model and efficient data-fetching capabilities. One of the more popular libraries for Go is graphql-go . While the library’s documentation is solid, details about project structure are often overlooked—yet project structure has a significant impact on maintainability and collaboration.

Over several years of building microservices with GraphQL in Go, I’ve tried all sorts of patterns and looked at many different codebases, both open-source and inside internal products. This article shares 8 ideal directory structures that, in my opinion, can make your graphql-go project more organized, scalable, and pleasant for a team to maintain.


1. The Foundation: A Modular Template, Not a Monolith

In short, a flat structure is a beginner’s trap. Small projects can survive it, but as soon as they grow, a single folder turns into a chaotic sea of files. A modular structure—based on domain or feature—gives you room to grow.

Here’s an ideal template you can use as a starting point:

plaintext
 1my-graphql-app/
 2├── cmd/
 3│   └── server/
 4│       └── main.go
 5├── internal/
 6│   ├── domain/
 7│   │   ├── user/
 8│   │   │   ├── model.go
 9│   │   │   ├── resolver.go
10│   │   │   └── repository.go
11│   │   └── post/
12│   │       ├── model.go
13│   │       ├── resolver.go
14│   │       └── repository.go
15│   ├── gql/
16│   │   ├── schema/
17│   │   │   └── schema.graphql
18│   │   ├── loader.go
19│   │   └── resolver.go
20│   ├── config/
21│   │   └── config.go
22│   └── pkg/
23│       ├── db/
24│       │   └── db.go
25│       ├── logger/
26│       │   └── logger.go
27│       └── utils/
28│           └── utils.go
29├── vendor/
30├── go.mod
31└── go.sum

2. cmd/ – A Clean Entry Point

Inside the cmd/ folder, we place the application’s entry point; typically one folder per binary. For a GraphQL server, create cmd/server/main.go. This is where the application starts, parses config, and runs the server.

Example:

go
 1package main
 2
 3import (
 4  "log"
 5  "net/http"
 6  "my-graphql-app/internal/gql"
 7  "my-graphql-app/internal/config"
 8)
 9
10func main() {
11  cfg := config.Load()
12  
13  srv := gql.NewGraphQLServer(cfg)
14  http.Handle("/query", srv)
15
16  log.Printf("Running at :%s...", cfg.Port)
17  log.Fatal(http.ListenAndServe(":" + cfg.Port, nil))
18}

3. internal/domain/ – Separate by Domain/Feature

Don’t lump all your resolvers into a single file. Use internal/domain/ to store resources per domain (for example: user, post). Each domain has:

  • model.go: Data structs matching the schema
  • repository.go: Data access abstraction
  • resolver.go: GraphQL resolver implementation specific to the domain

Example of internal/domain/user/model.go:

go
1package user
2
3type User struct {
4  ID    string
5  Name  string
6  Email string
7}

And its resolver:

go
 1package user
 2
 3import "context"
 4
 5type Resolver struct {
 6  Repo Repository
 7}
 8
 9// Example query resolver
10func (r *Resolver) GetUser(ctx context.Context, args struct{ID string}) (*User, error) {
11  return r.Repo.FindByID(ctx, args.ID)
12}

4. internal/gql/ – The Heart of GraphQL Logic

The gql/ folder is global to GraphQL, for example:

  • schema/: .graphql files, such as schema.graphql
  • loader.go: DataLoader functionality (when you need query efficiency)
  • resolver.go: The root resolver that delegates to each domain resolver

Example root resolver:

go
 1package gql
 2
 3import (
 4  "my-graphql-app/internal/domain/user"
 5  "my-graphql-app/internal/domain/post"
 6)
 7
 8type Resolver struct {
 9  User *user.Resolver
10  Post *post.Resolver
11}

5. internal/config/ – Managed Configuration

All configuration (port, env, and so on) goes here as a single source of truth, making testing and deployment easier.

go
 1package config
 2
 3import "os"
 4
 5type Config struct {
 6  Port string
 7  DBUrl string
 8}
 9
10func Load() *Config {
11  return &Config{
12    Port: os.Getenv("PORT"),
13    DBUrl: os.Getenv("DB_URL"),
14  }
15}

6. internal/pkg/ – Shared Helpers & Libraries

Store internal libraries, helpers, or infrastructure components (such as database utilities, loggers, JWT, and so on) here. Avoid the bad habit of a junk-drawer “helper.go” at the root filled with random stuff.

Example of a basic logger:

go
1package logger
2
3import "log"
4
5func Info(msg string) {
6  log.Printf("[INFO]: %s", msg)
7}

7. Testing: Keep Tests Close to Their Functions

Follow Go conventions and keep tests in _test.go files within the same folder. For GraphQL integration, create test cases that query the API.

Example: internal/domain/user/user_test.go

go
1func TestGetUser(t *testing.T) {
2  // Set up dummy repository and context
3  // Call resolver
4  // Assert output
5}

8. Clean Documentation & Schema

Don’t underestimate API documentation. Keep your GraphQL schema in internal/gql/schema/schema.graphql. Round it out with a description for every type/query.

Example:

graphql
 1"""User registered in the system"""
 2type User {
 3  "Unique user ID"
 4  id: ID!
 5  "User name"
 6  name: String!
 7  "User email"
 8  email: String!
 9}
10
11type Query {
12  "Fetch a user by ID"
13  user(id: ID!): User
14}

Simulating the Query Flow

Let’s visualize the flow with a Mermaid diagram:

MERMAID
flowchart LR
    Client["Client"]
    GQLHandler["GraphQL Handler (/query)"]
    RootResolver["Root Resolver"]
    UserDomain["User Resolver (Domain)"]
    DB["User Repository & Database"]

    Client --> GQLHandler
    GQLHandler --> RootResolver
    RootResolver --> UserDomain
    UserDomain --> DB

Once a GraphQL request comes in, the handler parses the schema, the request is delegated to the root resolver based on its type/query, then handed off to the domain resolver (for example: user), and finally forwarded to the repository to fetch data from the DB.


Folder Structure Reference Table

FolderContents & ResponsibilityExample File
cmd/Application entry pointmain.go
internal/domain/Business logic per domain/modeluser/model.go
internal/gql/GraphQL integration (schema & root resolver)schema.graphql, loader.go
internal/config/Centralized configurationconfig.go
internal/pkg/Internal utility/helper/infradb.go, logger.go
vendor/Library dependencies
go.mod, go.sumGo module management

Conclusion

Adopting a clean directory structure is a long-term investment. By separating concerns across domains, utilities, schema, and the entry point, we help the whole team maintain and scale the codebase. Don’t let your project structure be an afterthought. Start with a modular skeleton like the one above, then adapt it to the needs of real teams. A good structure = maintainability, scalability, and a happy team.

Want to discuss? Drop your experiences with GraphQL project structure in Go in the comments!


References:

Related Articles

💬 Comments