
OData is a standards-based way to ship queryable, discoverable APIs. go-odata wires the protocol...
title: Announcing go-odata v0.9.0: OData v4 APIs for Go published: true description: tags: go, odata, api, backend
OData is a standards-based way to ship queryable, discoverable APIs. go-odata wires the protocol details for you: metadata, key parsing, canonical URLs, and query option execution. In times of AI-assisted coding, having a strict, machine-readable standard like OData helps both humans and LLMs generate correct, predictable APIs.
Currently at v0.9.0, which effectively represents the planned 1.0 API surface. Remaining work toward 1.0 is focused on cleanup, documentation, and performance—not on major feature gaps.
$metadata, CRUD, navigation, property accessors, composite keys, singletons, and batch.$filter, $select, $expand, $orderby, $top/$skip, $count, $search, $apply, and delta tokens.package main
import (
"log"
"net/http"
"github.com/nlstn/go-odata"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
ID uint `json:"ID" gorm:"primaryKey" odata:"key"`
Name string `json:"Name" gorm:"not null" odata:"required"`
Description string `json:"Description"`
Price float64 `json:"Price" gorm:"not null"`
Category string `json:"Category" gorm:"not null"`
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&Product{})
service, err := odata.NewService(db)
if err != nil {
log.Fatal(err)
}
if err := service.RegisterEntity(&Product{}); err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle("/", service)
log.Println("Serving OData on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
With this minimal setup you automatically get:
GET / service document and GET /$metadata CSDL/Products, /Products(1) with optimistic concurrency via ETagsLifecycle hooks let you keep business logic close to your entities:
// Validate before writes
func (p *Product) ODataBeforeCreate(ctx context.Context, r *http.Request) error {
if p.Price < 0 {
return fmt.Errorf("price cannot be negative")
}
return nil
}
// Apply tenant filters before reads
func (p Product) ODataBeforeReadCollection(ctx context.Context, r *http.Request, opts *odata.QueryOptions) ([]func(*gorm.DB) *gorm.DB, error) {
tenantID := r.Header.Get("X-Tenant-ID")
if tenantID == "" {
return nil, fmt.Errorf("missing tenant header")
}
return []func(*gorm.DB) *gorm.DB{
func(db *gorm.DB) *gorm.DB { return db.Where("tenant_id = ?", tenantID) },
}, nil
}
No global middleware required—hooks are discovered via reflection and scoped to the entity.
$apply/$expand and other spec-required behaviors.v0.9.0 is focused on stability. The next steps before 1.0:
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...