Complete Guide to Building a Web API Server in Go|Design, Development Flow, Testing, and CI/CD Explained in Depth

  • golang
    golang
  • echo
    echo
  • gin
    gin
Published on 2023/11/21

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.GET defines 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:

https://shinagawa-web.com/en/blogs/go-gin-basic-guide

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:

https://shinagawa-web.com/en/blogs/go-gin-mvc-basics

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
  • :id is 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 :id from the DB
  • Return 204 No Content after deletion

Basic implementation flow

For any endpoint, the basic flow is:

  1. Set up routing (register endpoints with Gin or Echo)
  2. Receive the request (extract JSON and URL parameters)
  3. Execute business logic (Usecase layer)
  4. Save, fetch, or delete data in the DB (Repository layer)
  5. 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

  1. Start a test DB with Docker Compose
  2. Start the API server with test settings (e.g., APP_ENV=integration)
  3. Use go test to send requests to the API
  4. Check the data in the DB

This article introduces how to design and implement tests with Go × Gin × MVC:

https://shinagawa-web.com/en/blogs/go-gin-mvc-testing

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.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form