DEV Community

Nlstn
Nlstn

Posted on

Announcing go-odata v0.9.0: OData v4 APIs for Go

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.

Why OData?

  • Standardized querying ($filter, $expand, $apply) instead of custom query params
  • Machine-readable metadata ($metadata) enabling automatic client generation
  • Ecosystem support (Excel, Power BI, SAP, Dynamics, Graph, etc.)
  • Formal protocol instead of ad-hoc REST conventions

What go-odata provides

  • OData v4.01 coverage: 85+ compliance tests validating headers, payloads, query options, metadata, batch, change tracking, and errors.
  • End-to-end endpoints: Service document, $metadata, CRUD, navigation, property accessors, composite keys, singletons, and batch.
  • Query options implemented: $filter, $select, $expand, $orderby, $top/$skip, $count, $search, $apply, and delta tokens.
  • Database support via GORM: SQLite, PostgreSQL, MariaDB, and MySQL exercised in CI.
  • Lifecycle and read hooks: Validation, authorization, tenant scoping, redaction, and auditing without global middleware.
  • Custom operations: Bound/unbound actions and functions and virtual entities.

Getting started

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))
}
Enter fullscreen mode Exit fullscreen mode

With this minimal setup you automatically get:

  • GET / service document and GET /$metadata CSDL
  • CRUD endpoints: /Products, /Products(1) with optimistic concurrency via ETags
  • Full query option support, navigation, and property accessors

Extending behavior with hooks

Lifecycle 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
}
Enter fullscreen mode Exit fullscreen mode

No global middleware required—hooks are discovered via reflection and scoped to the entity.

Practical impact

  • Automatic metadata, key parsing, and canonical URLs remove protocol boilerplate.
  • Query option parsing/execution covers $apply/$expand and other spec-required behaviors.
  • ETag-based optimistic concurrency and per-entity HTTP method restrictions support safer writes.
  • Structured errors and tracing hooks aid debugging and observability.
  • Swappable databases through GORM without changing the API layer.

Road to 1.0

v0.9.0 is focused on stability. The next steps before 1.0:

  • Finalize any remaining breaking changes and API polish.
  • Broaden compliance coverage as the test suite grows.
  • Capture feedback from early adopters—file issues or discussions on GitHub.

Learn more

Top comments (0)