M
Mr Sugiarto
Developer
14 Oct 2026 6 min read

Sekarang bagian paling seru: mengubah notes-api.yaml dari Part 2 jadi kode Go yang benar-benar jalan.

Struktur Project

notesapi/
├── api/openapi/notes-api.yaml
├── internal/
│   ├── generated/notesapi/server.gen.go   # hasil generate, jangan diedit manual
│   ├── store/note_store.go                 # in-memory store
│   └── handler/note_handler.go             # implement ServerInterface
├── cmd/main.go
└── go.mod

Menjalankan oapi-codegen

oapi-codegen -package notesapi -generate types,fiber,spec \
  api/openapi/notes-api.yaml > internal/generated/notesapi/server.gen.go

Flag -generate types,fiber,spec menghasilkan tiga hal sekaligus dalam satu file:

  • types — struct Go untuk tiap schema (Note, CreateNoteRequest, dst)
  • fiber — ServerInterface dan helper untuk mendaftarkan route ke Fiber
  • spec — spec OpenAPI yang di-embed sebagai base64 (dipakai tooling lain yang butuh akses spec di runtime)

Membedah Hasil Generate

Dari 5 operation yang didefinisikan di notes-api.yaml, oapi-codegen menghasilkan interface ini:

// ServerInterface represents all server handlers.
type ServerInterface interface {
	// Daftar notes dengan pagination
	// (GET /notes)
	ListNotes(c *fiber.Ctx, params ListNotesParams) error
	// Buat note baru
	// (POST /notes)
	CreateNote(c *fiber.Ctx) error
	// Hapus note
	// (DELETE /notes/{id})
	DeleteNote(c *fiber.Ctx, id openapi_types.UUID) error
	// Detail 1 note
	// (GET /notes/{id})
	GetNote(c *fiber.Ctx, id openapi_types.UUID) error
	// Update note
	// (PUT /notes/{id})
	UpdateNote(c *fiber.Ctx, id openapi_types.UUID) error
}

Perhatikan: nama fungsi berasal dari operationId di YAML (listNotes → ListNotes), parameter path (id) dan query (page, limit, dibungkus jadi struct ListNotesParams) sudah tertulis lengkap dengan tipe datanya. Tidak ada satu pun yang kita ketik manual — semuanya hasil generate.

Types-nya juga sudah lengkap, persis mengikuti schema di YAML:

// Note defines model for Note.
type Note struct {
	Body      string             `json:"body"`
	CreatedAt time.Time          `json:"created_at"`
	Id        openapi_types.UUID `json:"id"`
	Title     string             `json:"title"`
	UpdatedAt time.Time          `json:"updated_at"`
}

// CreateNoteRequest defines model for CreateNoteRequest.
type CreateNoteRequest struct {
	Body  string `json:"body"`
	Title string `json:"title"`
}

Menulis Store (In-Memory)

Supaya fokus ke mekanisme OpenAPI tanpa perlu setup database dulu, Notes API pakai in-memory store yang thread-safe:

package store

import (
	"sync"
	"time"

	"github.com/google/uuid"
)

type Note struct {
	ID        uuid.UUID
	Title     string
	Body      string
	CreatedAt time.Time
	UpdatedAt time.Time
}

var ErrNotFound = &notFoundError{}

type notFoundError struct{}

func (e *notFoundError) Error() string { return "note not found" }

type NoteStore struct {
	mu    sync.RWMutex
	notes map[uuid.UUID]*Note
}

func NewNoteStore() *NoteStore {
	return &NoteStore{notes: make(map[uuid.UUID]*Note)}
}

func (s *NoteStore) Create(title, body string) *Note {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now().UTC()
	note := &Note{
		ID:        uuid.New(),
		Title:     title,
		Body:      body,
		CreatedAt: now,
		UpdatedAt: now,
	}
	s.notes[note.ID] = note
	return note
}

(Method Get, Update, Delete, List mengikuti pola yang sama — baca/tulis map di balik sync.RWMutex.)

Implementasi Handler

Ini bagian intinya: NoteHandler meng-implement notesapi.ServerInterface yang sudah digenerate.

package handler

import (
	"notesapi/internal/generated/notesapi"
	"notesapi/internal/store"

	"github.com/gofiber/fiber/v2"
	openapitypes "github.com/oapi-codegen/runtime/types"
)

// NoteHandler implements notesapi.ServerInterface, generated from
// api/openapi/notes-api.yaml. The compiler enforces this: if the YAML
// contract changes and the generated interface changes with it, this
// struct stops satisfying notesapi.ServerInterface until it's updated.
type NoteHandler struct {
	store *store.NoteStore
}

func NewNoteHandler(s *store.NoteStore) *NoteHandler {
	return &NoteHandler{store: s}
}

var _ notesapi.ServerInterface = (*NoteHandler)(nil)

func (h *NoteHandler) ListNotes(c *fiber.Ctx, params notesapi.ListNotesParams) error {
	page := 1
	if params.Page != nil {
		page = *params.Page
	}
	limit := 10
	if params.Limit != nil {
		limit = *params.Limit
	}

	notes, total := h.store.List(page, limit)
	data := make([]notesapi.Note, 0, len(notes))
	for _, n := range notes {
		data = append(data, toNoteResponse(n))
	}

	return c.JSON(fiber.Map{
		"success": true,
		"message": "Notes retrieved successfully",
		"data":    data,
		"meta": notesapi.PaginationMeta{
			Page:  &page,
			Limit: &limit,
			Total: &total,
		},
	})
}

Baris var _ notesapi.ServerInterface = (*NoteHandler)(nil) adalah compile-time check: kalau NoteHandler belum mengimplementasikan semua method yang diminta interface, go build langsung gagal di baris ini — bukan nanti pas di-panggil di runtime. Inilah maksud "compiler ikut menjaga kontrak" yang disebut di Part 1.

Method CreateNote, GetNote, UpdateNote, DeleteNote mengikuti pola serupa: parse request (kalau ada), panggil method store yang sesuai, kembalikan response JSON dengan status code yang sesuai (201 untuk create, 204 untuk delete, 404 kalau tidak ditemukan).

Menyambungkan ke Fiber

package main

import (
	"log"

	"notesapi/internal/generated/notesapi"
	"notesapi/internal/handler"
	"notesapi/internal/store"

	"github.com/gofiber/fiber/v2"
)

func main() {
	app := fiber.New()

	noteStore := store.NewNoteStore()
	noteHandler := handler.NewNoteHandler(noteStore)

	notesapi.RegisterHandlers(app, noteHandler)

	log.Fatal(app.Listen(":8080"))
}

notesapi.RegisterHandlers — juga hasil generate — otomatis mendaftarkan kelima route (GET /notes, POST /notes, GET/PUT/DELETE /notes/{id}) ke Fiber app, lengkap dengan parsing path/query parameter sebelum memanggil method handler kita.

Menjalankan dan Menguji Sungguhan

go run ./cmd/main.go

Lalu dicoba satu-satu lewat curl. Berikut hasil sebenarnya dari server yang jalan:

Create:

$ curl -X POST localhost:8080/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"Belajar OpenAPI","body":"Spec-first itu enak"}'
{"data":{"body":"Spec-first itu enak","created_at":"2026-10-08T20:06:23.247416Z","id":"f991a953-41e8-4655-acca-eb0654760646","title":"Belajar OpenAPI","updated_at":"2026-10-08T20:06:23.247416Z"},"message":"Note created successfully","success":true}

List (dengan pagination meta):

$ curl "localhost:8080/notes?page=1&limit=10"
{"data":[{"body":"Spec-first itu enak","created_at":"2026-10-08T20:06:23.247416Z","id":"f991a953-41e8-4655-acca-eb0654760646","title":"Belajar OpenAPI","updated_at":"2026-10-08T20:06:23.247416Z"}],"message":"Notes retrieved successfully","meta":{"limit":10,"page":1,"total":1},"success":true}

Update:

$ curl -X PUT localhost:8080/notes/f991a953-41e8-4655-acca-eb0654760646 \
  -H "Content-Type: application/json" \
  -d '{"title":"Belajar OpenAPI (updated)","body":"Spec-first makin enak"}'
{"data":{"body":"Spec-first makin enak","created_at":"2026-10-08T20:06:23.247416Z","id":"f991a953-41e8-4655-acca-eb0654760646","title":"Belajar OpenAPI (updated)","updated_at":"2026-10-08T20:06:23.318524Z"},"message":"Note updated successfully","success":true}

Delete lalu Get lagi (membuktikan 404 dan 204 benar-benar dikembalikan):

$ curl -o /dev/null -w "%{http_code}\n" -X DELETE localhost:8080/notes/f991a953-41e8-4655-acca-eb0654760646
204
$ curl -o /dev/null -w "%{http_code}\n" localhost:8080/notes/f991a953-41e8-4655-acca-eb0654760646
404

Semua jalan sesuai kontrak yang kita tulis di notes-api.yaml: status code, bentuk response, dan field-nya — semua persis seperti yang didefinisikan di spec.

Di Part 5, kita validasi spec ini secara otomatis pakai linter, dan serve dokumentasi interaktif supaya endpoint-endpoint ini bisa dicoba langsung dari browser tanpa curl.

M
Mr Sugiarto

Developer

Bagian dari Series: OpenAPI Spec-First untuk Go Developer

Seri 8 bagian: belajar mendesain REST API dengan pendekatan spec-first di Go - menulis kontrak OpenAPI dulu, generate kode, lalu terapkan ke project h...

Lihat Series Lengkap