DEV Community

medunes
medunes

Posted on

Creating a REST API in Go with Gin: A Pragmatic, Spec-First Guide

Let's cut to the chase. You need to build a REST API in Go. You want it to be modern, maintainable, and built following best practices: this is a hands-on guide to doing it the right way.

The Problem: Specification Hell or Heaven?

So, you're starting a greenfield project. The eternal question arises: do you write the code first and then generate the API specification (like OpenAPI/Swagger)? Or do you write the spec first and then generate the code?

This isn't just a philosophical debate. It has real-world consequences:

  • Code-First: You move fast initially, but the spec often becomes an afterthought. It drifts from the actual implementation, leading to confused frontend teams, incorrect documentation, and integration nightmares.
  • Spec-First: This seems slower upfront, but it forces you to think through your API design. The spec becomes the source of truth.

Our Approach: We're going all-in on spec-first. Why? Because it unlocks massive benefits. By creating an openapi.yaml file first, we establish a contract. Other teams (frontend, QA, other backend services) can immediately use this contract to generate mock servers and start their work. They aren't blocked by our implementation. We are all working in parallel, protected by a clear, agreed-upon interface.

This also sets us up to leverage powerful code generators. We can automatically create server stubs, data models, and even client-side code, all from that single YAML file. This isn't just efficient; it's a key to long-term maintainability.

The Plan: Spec -> Generate -> Implement

Here’s how we're going to tackle this:

  1. Define the API Contract: We'll write an openapi.yaml file. This is our single source of truth.
  2. Structure the Project: We'll lay out our Go project in a clean, idiomatic way.
  3. Generate Code: Using oapi-codegen, we'll generate the server scaffolding and data types directly from our YAML file.
  4. Implement Business Logic: We'll write the actual logic for our API endpoints.
  5. Automate with Make: We'll use a Makefile to streamline the code generation process, making it a repeatable, one-command step.

Project Structure: Go Standard Layout

A good project structure is crucial for maintainability. We'll follow a layout that is common in the Go community and promotes a clean separation of concerns.

/my-api
├── cmd/
│   └── api/
│       └── main.go         # Entry point of our application
├── docs/
│   └── openapi.yaml        # Our API specification (source of truth)
├── internal/
│   ├── api/
│   │   ├── openapi_server.gen.go  # Generated server code
│   │   └── openapi_types.gen.go   # Generated type definitions
│   └── handler/
│       └── handler.go      # Our business logic implementation
├── go.mod
├── go.sum
└── Makefile
Enter fullscreen mode Exit fullscreen mode
  • cmd/api/main.go: This is where our application starts. Its job is to initialize the database connections, set up the Gin router, and wire everything together.
  • docs/openapi.yaml: The heart of our spec-first approach.
  • internal/: This is a special directory in Go. Code inside internal can only be imported by code within the same project, preventing other projects from accidentally depending on our internal logic.
    • api/: This package will hold the code generated by oapi-codegen. We keep it separate to make it clear what is machine-generated vs. hand-written.
    • handler/: This is where we'll write the actual implementation for our API endpoints (the "business logic").
  • Makefile: A simple script to automate our development tasks, like code generation.

Step 1: Define the API with OpenAPI 3

First, let's create our API specification in docs/openapi.yaml. We'll define two simple endpoints: one to get a list of users and another to get a list of products.

# docs/openapi.yaml
openapi: 3.0.3
info:
  title: "Simple API"
  version: "1.0.0"
paths:
  /users:
    get:
      summary: "Get a list of users"
      operationId: "getUsers"
      responses:
        '200':
          description: "A list of users"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
  /products:
    get:
      summary: "Get a list of products"
      operationId: "getProducts"
      responses:
        '200':
          description: "A list of products"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
    Product:
      type: object
      properties:
        sku:
          type: string
        name:
          type: string
        price:
          type: number
          format: float
Enter fullscreen mode Exit fullscreen mode

Step 2: Automate Code Generation with a Makefile

Now for the magic. We'll use oapi-codegen to read our YAML file and generate Go code. With Go 1.24+, we can manage tools like this directly in our go.mod file, which is fantastic for ensuring consistent builds.

First, add the tool to your project:

go get -u github.com/gin-gonic/gin
go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
Enter fullscreen mode Exit fullscreen mode

This command does two things:

  1. Adds gin as a project dependency.
  2. Adds oapi-codegen as a tracked tool in go.mod.

Now, create the Makefile:

# Makefile
.PHONY: gen-code

gen-code:
    @echo "--- Generating OpenAPI server and types ---"
    @go tool oapi-codegen --config=codegen.yaml docs/openapi.yaml

.PHONY: tidy
tidy:
    @go mod tidy
Enter fullscreen mode Exit fullscreen mode

For more complex projects, using a configuration file for oapi-codegen is cleaner. Create codegen.yaml:

# codegen.yaml
package: api
generate:
  - types
  - gin
output: internal/api/openapi.gen.go
Enter fullscreen mode Exit fullscreen mode

This configuration tells the tool to generate the types and gin server code into a single file. Now, running make gen-code will generate our server interface and all the required structs into internal/api/openapi.gen.go.


Step 3: Implement the Business Logic

The generated code provides us with a ServerInterface. Our job is to create a struct that implements this interface. This is where our actual business logic lives.

Create the file internal/handler/handler.go:

// internal/handler/handler.go
package handler

import (
    "net/http"

    "my-api/internal/api" // Import the generated package

    "github.com/gin-gonic/gin"
)

// Server implements the generated ServerInterface.
type Server struct{}

// Make sure we conform to the interface
var _ api.ServerInterface = (*Server)(nil)

// getUsers implements the business logic for the /users endpoint.
func (s *Server) GetUsers(c *gin.Context) {
    users := []api.User{
        {Id: 1, Name: "Alice"},
        {Id: 2, Name: "Bob"},
    }
    c.JSON(http.StatusOK, users)
}

// getProducts implements the business logic for the /products endpoint.
func (s *Server) GetProducts(c *gin.Context) {
    products := []api.Product{
        {Sku: "A123", Name: "Laptop", Price: 1200.50},
        {Sku: "B456", Name: "Mouse", Price: 25.00},
    }
    c.JSON(http.StatusOK, products)
}
Enter fullscreen mode Exit fullscreen mode

Notice how we're using the api.User and api.Product structs that were generated for us. If we change the spec and regenerate, the compiler will tell us exactly what we need to update in our handler. This is a remarkable win for maintainability.


Step 4: Wire It All Together

Finally, let's write our main.go to start the server.

// cmd/api/main.go
package main

import (
    "my-api/internal/api"
    "my-api/internal/handler"

    "github.com/gin-gonic/gin"
)

func main() {
    // Create our handler which satisfies the generated interface
    myHandler := &handler.Server{}

    // Set up the Gin router
    r := gin.Default()

    // Use the generated RegisterHandlers function to wire up our handlers
    // to the Gin router.
    api.RegisterHandlers(r, myHandler)

    // Start the server
    r.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

The api.RegisterHandlers function is another powerful piece of the generated code. It takes care of all the boilerplate for routing requests to the correct handler methods.


Conclusion: A Maintainable, Scalable Foundation

And that's it! You now have a fully functional, spec-driven REST API.

Let's recap the benefits of this approach:

  • Single Source of Truth: openapi.yaml is the contract. All code is generated or validated against it.
  • Parallel Development: Frontend and backend teams can work simultaneously, unlocked by the API spec.
  • Type Safety: Go's strong type system, combined with code generation, catches errors at compile time, not runtime.
  • Reduced Boilerplate: oapi-codegen handles the tedious work of routing and data model creation.
  • Consistency: The Makefile and go tool support ensure that every developer on your team generates code the exact same way.

This spec-first methodology provides a robust foundation for building APIs in Go that are not only fast and efficient but also a pleasure to maintain and scale over time. Happy coding!

References

Top comments (0)