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—ServerInterfacedan helper untuk mendaftarkan route ke Fiberspec— 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 = ¬FoundError{}
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.
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