Modular Architecture in Go with REST and GraphQL Sharing the Same Core
Learn how to structure scalable and maintainable Go applications using a clean, modular architecture. This article includes a practical example with REST and GraphQL sharing the same core logic.

When building Go applications, a common challenge is finding a structure that balances simplicity, flexibility, and maintainability.
Although Go doesnβt enforce a specific architectural style, successful projects often adopt some common practices like separating the code into internal
, cmd
, and pkg
directories.
This article presents a modular architecture proposal that facilitates project evolution and allows you to add or switch interfaces (REST, GraphQL, CLI, gRPC) without rewriting your business logic. To demonstrate the concept, weβll implement a simple blog Article CRUD available via both REST (Fiber) and GraphQL (gqlgen).
π§© Why Modularization Matters
- Reuse business logic across different interfaces
- Facilitate testing by isolating rules
- Reduce coupling with frameworks and libraries
- Allow safe evolution, such as switching Fiber to Gin or REST to gRPC
π Proposed Structure
blog-api/
βββ cmd/ # Entrypoints (REST and GraphQL)
β βββ rest/ # REST server using Fiber
β βββ graphql/ # GraphQL server using gqlgen
βββ internal/
β βββ domain/ # Entities and interfaces
β βββ service/ # Business logic
β βββ database/ # Repository implementations
βββ graph/ # GraphQL schema and resolvers
βββ pkg/ # Shared utilities (e.g., logger)
π οΈ Practical Example: Blog - Article CRUD
Here's the structure of the entity:
type Article struct {
ID string
Title string
Content string
Author string
CreatedAt time.Time
}
We implemented methods such as CreateArticle
, ListArticles
, GetArticleByID
, UpdateArticle
, and DeleteArticle
, which are exposed through REST and GraphQL interfaces β sharing the same business logic.
-> REST with Fiber
app.Post("/articles", func(c *fiber.Ctx) error {
var input service.CreateArticleInput
if err := c.BodyParser(&input); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid input"})
}
article, err := svc.CreateArticle(context.Background(), input)
return c.Status(201).JSON(article)
})
-> GraphQL with gqlgen
Example of the schema:
type Query {
articles: [Article!]!
article(id: ID!): Article
}
type Mutation {
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Boolean!
deleteArticle(id: ID!): Boolean!
}
Resolver example:
func (r *mutationResolver) CreateArticle(ctx context.Context, input CreateArticleInput) (*Article, error) {
return r.ArticleService.CreateArticle(ctx, input)
}
β Outcomes
- Test logic without running the server
- Reuse code safely
- Swap frameworks without rewriting business logic
- Keep a clear separation between domain, infrastructure, and interfaces
... disclaimer |
This is a simple and educational project focused on structure and modularization. It is not production-ready, but can be extended with:
- Docker and Makefile for local setup
- Environment variables with
.env
files - Unit tests and real database integration
- Structured logging, input validation, and authentication
π GitHub ->
Full code available here: github.com/betonr/golang-base-structure
βοΈ Final Thoughts
While thereβs no single architecture that fits all cases, starting with a clean, modular foundation helps ensure long-term maintainability and adaptability. Whether for REST, GraphQL, or future extensions like gRPC, this kind of separation enables sustainable growth.
Questions, feedback, or contributions? π« Reach out or open an issue on GitHub!