Complete Guide to Building a Web API Server in Go|Design, Development Flow, Testing, and CI/CD Explained in Depth
You might want to build a Web API server in Go but feel unsure about how to proceed.
Here, the steps are explained in order—from design concepts to setup procedures—so that even beginners can follow along easily.
Key points to understand first
When building a Web API server in Go, keeping the following two points in mind will make development much smoother.
✅ Emphasize loose coupling & maintainability
✅ Separate business logic (domain) from infrastructure concerns
If you keep this in mind, it will be much easier to extend features later.
What does it mean to emphasize loose coupling & maintainability?
Loose coupling means designing the parts of your application so that they “don’t depend too heavily on each other.”
For example, if you tightly couple the routing part of your API (Gin, etc.) directly with database operations (GORM, etc.), it tends to become difficult to modify or extend.
Instead, if you separate responsibilities into layers such as API layer (handlers), business logic layer (Usecase), and data access layer (Repository), you get benefits like:
- Even if you modify one part, the impact on other parts is small
- It’s easier to write tests per responsibility (e.g., mock the DB and test only the business logic)
Separate business logic (domain) from infrastructure
-
Business logic is the part that defines “how this particular app should behave.”
Example: When creating an album, the title is required, check that the category is valid, etc. -
Infrastructure refers to the parts that interact with external systems.
Example: Saving data to the DB, API routing, file operations, etc.
What happens when you separate these?
For example, if you decide “I want to switch the DB from MySQL to PostgreSQL!”, in many cases you only need to modify the infrastructure part (Repository).
Organize requirements and specifications
First, summarize what kind of API you want to build and what kind of data you will handle.
Even a simple ER diagram or data flow diagram will make development smoother.
Think about what kind of API you want to build
Roughly list the purpose and features of the API.
For a blog site, you might have operations like:
- API to post a new blog article
- API to fetch posted articles
- API to edit an article
- API to delete an article
- API to manage categories and tags
If you think in terms of “What operations are needed?” and list the API endpoints (URLs), things will go smoothly.
Decide what data you will handle
Next, organize how you will handle blog article data.
For a blog site, the basic elements of article data might look like this:
- Article title (Title)
- Article body (Content)
- Publication date (PublishedAt)
- Category (Category)
- Tags (Tags)
- Author information (Author)
If you keep in mind “What data will this API return and accept?”, it becomes easier to decide the shape of responses and requests.
Draw ER diagrams and data flows
If you turn the “image in your head” into diagrams here, the overall picture becomes much clearer.
- ER diagram (Entity-Relationship diagram)
For example, the relationship between articles and categories can be drawn like this:
Post (ID, Title, Content, PublishedAt, AuthorID, CategoryID)
Category (ID, Name)
Author (ID, Name)
- Data flow diagram
It’s useful to draw how the API processes data in a flow.
Client → API server (endpoint) → Business logic → DB
Why is it important to do this thoroughly?
- You won’t get stuck later wondering “What does this article API return again?”
- It’s easier to share understanding with the frontend or other team members
- It can serve as design documentation, making future maintenance easier
Minimal behavior check (Hello World)
If you try to build a complex API right away, you’re likely to get stuck somewhere.
First, confirm the minimal behavior: “The server starts and accepts requests.”
In Go, Gin and Echo are popular frameworks.
Both let you try out APIs with simple code.
Gin sample
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default() // Gin default settings (logging and recovery enabled)
// Create /health endpoint
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// Start server (port 8080)
r.Run(":8080")
}
✅ Points
r.GETdefines a “route that responds to GET requests.”- When you access
/health, it returns JSON{"status": "ok"}.
If you want to learn more about the basics of Gin, see this article:
Echo sample
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New() // Create Echo instance
// Create /health endpoint
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
// Start server (port 8080)
e.Start(":8080")
}
✅ Points
- The “GET endpoint” is defined in a similar way to Gin.
- You can return JSON responses with c.JSON.
- Echo makes it easy to build JSON using
map[string]string, so it’s also very convenient.
For now, it’s fine if /health responds instead of “Hello, World!”.
Once you get this far, you can be confident that “the server can start.”
At first, it’s enough just to confirm that “the server starts.”
By testing whether routing works correctly and responses are returned properly, you can establish your environment setup and basic code structure.
Decide on design principles
When building an API server, if you think about “what design principles to follow” at the beginning, you’ll end up with a flexible and robust app that can handle future changes.
Why are design principles important?
For a small app, you can “put everything in a single file” and it will still run, but as features grow, you will definitely run into trouble.
- You don’t know where anything is written…
- A small change breaks another feature…
- It’s hard to test…
To avoid these problems, you need to firmly decide on your design principles—the skeleton of your project.
Representative design styles
Clean architecture (layered architecture)
By separating responsibilities into layers, dependencies become clear and maintainability improves.
├── handler (API entry point)
├── usecase (Business logic)
├── entity (Domain models)
├── gateway (DB and external API interactions)
├── pkg (Shared libraries and utilities)
✅ Points
- For example, if you decide “I want to switch the DB from MySQL to PostgreSQL,” you only need to modify gateway
- usecase and entity protect the “core logic of the app,” making it resilient to infrastructure changes
MVC (simple structure)
A structure that separates responsibilities into “Model, View, Controller.” For small apps, this is perfectly fine.
├── controllers (Part that receives requests)
├── models (DB and data structures)
├── views (HTML templates, etc.)
✅ Points
- Each file doesn’t become huge, so the codebase is easier to understand
- You don’t need to be as strict as with clean architecture
This article introduces the basic flow of building a blog post API using Go × Gin with an MVC structure:
Tips for deciding on design principles
-
Can you visualize “what happens where”?
If you can imagine things like “This processing is done in usecase” and “DB operations are in gateway,” you’ll have fewer doubts. -
You don’t need to aim for perfection from the start
Just separate things for now, and later you can review: “Let’s merge this,” “Let’s add another layer here,” etc. Of course, you can also migrate from MVC to clean architecture partway through.
Decide the API schema (use OpenAPI if possible)
When building a Web API, “How do we document the API specification?” is a very important point.
Especially for team development or when collaborating with frontend/mobile, using OpenAPI (formerly Swagger) is highly recommended.
What is an API schema?
An API schema is the “blueprint of the API,” covering things like:
- What endpoints exist?
- What request bodies and responses exist?
- What data types and validations are needed?
Instead of writing this in prose, OpenAPI specifies it in a machine-readable format (YAML or JSON).
Benefits of OpenAPI (Swagger)
-
Anyone can view it as documentation
With swagger-ui, it can be displayed clearly in the browser, so frontend developers, designers, and testers won’t get lost. -
Manage everything with tools
For example, using oapi-codegen, you can automatically generate Go types (structs and client code) from OpenAPI. -
Easy to create tests and mock servers
With OpenAPI, you can use tools that automatically spin up mock servers (like Prism).
This makes it possible to “start frontend development before the backend is ready.”
Example: Contents of an OpenAPI file (e.g., openapi.yaml)
openapi: 3.0.0
info:
title: My Blog API
version: 1.0.0
paths:
/posts:
get:
summary: Get list of articles
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Post'
components:
schemas:
Post:
type: object
properties:
id:
type: integer
title:
type: string
content:
type: string
publishedAt:
type: string
format: date-time
Automatic Go code generation (oapi-codegen, etc.)
oapi-codegen is a commonly used tool for generating Go code from OpenAPI.
- Generates Go types (structs for requests and responses) from openapi.yaml
- Can also generate API clients and server stubs
✅ Benefits
- Since types are generated automatically, you can respond immediately to API spec changes
- Reduces hand-written mistakes and helps prevent bugs
Implement endpoints one by one
Start small
If you’re building a blog API, trying to implement everything at once is tough.
Start by implementing one endpoint at a time, such as “only the create API.”
Example endpoints to implement
✅ POST /blogs (create)
- API to register a new blog article
- Data received: title, body, category, etc.
- Response on success: ID and details of the created article
✅ GET /blogs/:id (retrieve)
- API to get article details
:idis the article ID (URL parameter)- Fetch that article from the database and return it
✅ PATCH /blogs/:id (update)
- API to edit an article
- Include only the fields you want to update in the request body
- Update that article in the DB and return it
✅ DELETE /blogs/:id (delete)
- API to delete an article
- Delete the article with the specified
:idfrom the DB - Return 204 No Content after deletion
Basic implementation flow
For any endpoint, the basic flow is:
- Set up routing (register endpoints with Gin or Echo)
- Receive the request (extract JSON and URL parameters)
- Execute business logic (Usecase layer)
- Save, fetch, or delete data in the DB (Repository layer)
- Return the response as JSON
Middleware and error handling
If you’re going to operate an API server in practice, there are some essential features.
Middleware is what handles these in a unified way.
What is middleware?
Middleware runs before the request reaches the handler and right before the response is returned.
Since it’s “intermediate processing” applied across the entire server, you don’t need to write the same logic repeatedly for multiple endpoints.
Examples of essential middleware
✅ CORS (Cross-Origin Resource Sharing)
If you call an API from a browser to a different domain (origin), the browser will block the request if CORS is not configured.
For example:
- Frontend: http://localhost:3000
- API: http://localhost:8080
In this case, the browser treats it as a “different origin” and blocks the request.
By adding CORS middleware, you can centrally manage server-side settings like “Allow requests from this origin.”
✅ Logging
A mechanism to record what requests came in and how the server responded when the API was called.
- Example: Gin’s ginzap (integrates with the Zap logger), etc.
With logs…
- It’s easier to track down the cause when something goes wrong
- You can see when, who, and which API was called
✅ Panic recovery
In Go apps, sometimes a panic (fatal error) occurs.
If you don’t handle it, the entire server can crash.
Panic recovery middleware automatically catches panics and prevents the server from going down.
It also logs the error, making it easier to investigate the cause.
In Gin, you can configure it like this:
r.Use(middleware.CorsMiddleware())
r.Use(middleware.GinZap())
r.Use(middleware.RecoveryWithZap())
Set up testing
Testing is how you automatically verify that the API you built “works correctly” and “has no bugs.”
If you write tests, you can make changes with confidence and easily see “what broke” later.
Unit tests
Test business logic and utilities at the level of a single function or class.
- When creating a new article, does it return an error if the title is empty?
- Does the validation for the publication date work correctly?
etc.
✅ Points
- Do not connect to the DB or external APIs (fast and stable!)
- Makes it easy to pinpoint the cause of bugs
func TestIsValidTitle(t *testing.T) {
valid := IsValidTitle("My Blog Post")
if !valid {
t.Error("expected title to be valid")
}
}
Integration tests
Tests that verify whether multiple components work together. For APIs, you run things like:
- Handlers
- Business logic
- DB access
For example:
- Does POST /blogs actually create an article in the DB?
- Does GET /blogs/:id correctly retrieve the created article?
✅ Points
- Since the DB is also running, you can verify that the app works as a whole
- Slightly heavier than unit tests
E2E tests (End-to-End tests)
Tests that hit the entire API server “from the outside.”
Examples:
- Post an article from the frontend and check that it appears in the list
- Verify the entire flow from a “user’s perspective” in an environment close to production
✅ Points
- Since it’s close to the production environment, it serves as the “final guarantee before release”
- Heavy, so you don’t need to cover every case here
Tool examples:
- You can write them in Go using net/http
- Playwright or Cypress are also commonly used
Flow for unit tests
- Don’t depend on the DB; create mocks (e.g., Go’s testify/mock)
- Test only the Usecase functions
Flow for integration tests
- Start a test DB with Docker Compose
- Start the API server with test settings (e.g., APP_ENV=integration)
- Use go test to send requests to the API
- Check the data in the DB
This article introduces how to design and implement tests with Go × Gin × MVC:
How to get started?
Start with unit tests for business logic.
They’re quick to write and give you a guarantee that “this part is correct,” so they’re the easiest entry point.
Once you’re used to that, use integration tests to verify the behavior of the entire API.
For example, check whether “POST /blogs really creates an article in the DB.”
Before releasing to production, run E2E tests for a final check.
You can test in a state that’s like “real users are using it,” which gives you peace of mind.
Introduce CI/CD
Once your development flow stabilizes, it’s a good idea to introduce CI (Continuous Integration) and CD (Continuous Delivery or Deployment).
This eliminates “manual procedural mistakes” and lets you run tests and builds automatically.
What is CI/CD?
CI (Continuous Integration)
- “A mechanism that automatically runs tests and builds every time code is merged”
- Gives you the reassurance that “Tests are passing, so nothing is broken!”
CD (Continuous Delivery or Deployment)
- “A mechanism that automatically deploys a working app to the production environment”
- Example: When a PR is merged, it’s automatically deployed to production.
What should you automate?
For a Go API server, you might automate tasks like:
Running tests
- Automatically run go test ./...
- Prevent PRs from being merged if tests fail
Linting and static analysis
- Automatically run static checks with golangci-lint, etc.
- Keep code quality consistent
Docker build and smoke tests
- Build images with docker build and start them
- If needed, run integration tests with docker-compose up including a test DB
Deployment (as needed)
- Automate deployment to production or staging
- Example: Deploy to production when changes are merged into the main branch
Benefits of introducing CI/CD
Reduces human error
- No more “I forgot to run tests!” or “I misconfigured something!”
Confidence when merging PRs
- Everyone can confirm that “all tests are passing”
Fewer production issues
- Since the release steps to production are automated, safety improves!
First step
CI/CD might seem difficult, but starting with “just automate tests” is more than enough as a first step.
For example, with GitHub Actions, you can get started by adding a minimal configuration like this:
name: Go CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.19"
- run: go test ./...
Summary
When building a Web API server in Go…
✅ Start small with “Hello, World!”
✅ Decide on a design and implement APIs step by step
✅ Set up middleware and tests to improve reliability
✅ As you build out the system, you’ll be strong even in team development
It’s important to gradually stack up “working pieces” in this order.
Don’t aim for perfection from the start; instead, start small and grow the system over time.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
CI/CD Strategies to Accelerate and Automate Your Development Flow: Leveraging Caching, Parallel Execution, and AI Reviews
2024/03/12Complete Cache Strategy Guide: Maximizing Performance with CDN, Redis, and API Optimization
2024/03/07Getting Started with Building Web Servers Using Go × Echo: Learn Simple, High‑Performance API Development in the Shortest Time
2023/12/03Go × Gin Advanced: Practical Techniques and Scalable API Design
2023/11/29Go × Gin Basics: A Thorough Guide to Building a High-speed API Server
2023/11/23Article & Like API with Go + Gin + GORM (Part 1): First, an "implementation-first controller" with all-in-one CRUD
2025/07/13Building an MVC-Structured Blog Post Web API with Go × Gin: From Basics to Scalable Design
2023/12/03Robust Test Design and Implementation Guide in a Go × Gin × MVC Architecture
2023/12/04