gocrud

package module
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 1, 2026 License: MIT Imports: 11 Imported by: 0

README

go-crud

A lightweight, generic CRUD library for Go — designed to eliminate repetitive boilerplate code without the overhead of a full ORM.

Overview

go-crud provides a simple, flexible interface for defining CRUD operations in Go.

It’s ideal if you:

  • Don’t want to hand-write the same Create, Read, Update, and Delete logic for each model.
  • Don’t need (or want) the complexity of large ORM frameworks.
  • Prefer to stay close to standard database/sql.

Inspired by Andrew Pillar’s excellent article: 👉 A Simple CRUD Library for PostgreSQL with Generics in Go

Although designed to work with any SQL database, it has primarily been tested with PostgreSQL. If you encounter issues or have improvements, please open an issue or PR — feedback is very welcome!

Usage

You can use go-crud in two ways:

  1. With reflection (simpler setup, slightly slower)
  2. Without reflection (manual setup, marginally faster)
1. Using Reflection

Each CRUD action maps to a generic repository method. To get started, define a new model and embed the gocrud.Reflection interface in your model.

type ModelWithReflection struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Type  string `json:"type"`
	IP    string `json:"ip"`
	Actions []string `json:"actions"`
	gocrud.Reflection
}

repo := gocrud.NewGenericRepository(db, "table_name", func() *ModelWithReflection {
	return &ModelWithReflection{}
})

got, err := repo.Get(ctx, id)
Why the embedded interface?

gocrud.Reflection interface provides a StructToMap(interface{}) map[string]any method that converts your struct into a map of field_name:pointer_to_field using Go’s reflection.

The map keys, which are struct field names, are used to validate against column names returned by the query.

While map values, which are pointers to struct fields, are passed to rows.Scan() so your model can be populated.


2. Without Reflection

If you prefer to avoid reflection, you can implement StructToMap yourself. In benchmarks, the performance difference is negligible but this approach gives you full control.

type ModelWithoutReflection struct {
	ID      int      `json:"id"`
	Name    string   `json:"name"`
	Type    string   `json:"type"`
	IP      string   `json:"ip"`
	Actions []string `json:"actions"`
}

func (o *ModelWithoutReflection) StructToMap(interface{}) map[string]any {
	return map[string]any{
		"id":      &o.ID,
		"name":    &o.Name,
		"type":    &o.Type,
		"ip":      &o.IP,
		"actions": &o.Actions,
	}
}

repo := gocrud.NewGenericRepository(db, "table_name", func() *ModelWithoutReflection {
	return &ModelWithoutReflection{}
})

got, err := repo.Get(ctx, id)

Initialization

You can create a new generic repository using:

gocrud.NewGenericRepository[M gocrud.Model](
	db *sql.DB,
	table string,
	callback func() M,
) *Repository[M]
Parameters
  1. db — your *sql.DB connection
  2. table — the name of the database table to target
  3. callback — a function returning a new instance of your model
Why the Callback?

The callback allows the repository methods to initialize a new instance of the concrete type (your model) at runtime. For example, the Get() method calls the callback to create a fresh model instance to fill it with data retrieved from the database.

Case in point:

repo := gocrud.NewGenericRepository(db, "users", func() *User { return &User{} })

Now, whenever the repository needs to return a User, it executes the callback to allocate a new one.

HTTP Route Registration

go-crud provides helper functions to register HTTP handlers for your CRUD operations:

mux := http.NewServeMux()
repo := gocrud.NewGenericRepository(db, "items", func() *Item { return &Item{} })
errw := gocrud.DefaultErrorWriter{}

gocrud.RegisterCreate("POST /items", mux, repo.Create, errw)
gocrud.RegisterGet("GET /items/{id}", mux, repo.Get, errw)
gocrud.RegisterGetAll("GET /items", mux, repo.GetAll, errw)
gocrud.RegisterDelete("DELETE /items/{id}", mux, repo.Delete, errw)
gocrud.RegisterUpdate("POST /items/{id}", mux, repo.Update, errw)
Custom Error Handling

The ErrorWriter interface allows you to customize how errors are returned to clients:

type ErrorWriter interface {
    WriteError(w http.ResponseWriter, r *http.Request, err error, customMsg string, statusCode int)
}

The DefaultErrorWriter uses the custom message if provided, otherwise falls back to err.Error():

type DefaultErrorWriter struct{}

func (d DefaultErrorWriter) WriteError(w http.ResponseWriter, _ *http.Request, err error, customMsg string, statusCode int) {
    if customMsg != "" {
        http.Error(w, customMsg, statusCode)
        return
    }
    http.Error(w, err.Error(), statusCode)
}

To implement custom error handling (e.g., JSON responses, logging), create your own type that implements ErrorWriter:

type JSONErrorWriter struct {
    Logger *log.Logger
}

func (j JSONErrorWriter) WriteError(w http.ResponseWriter, r *http.Request, err error, customMsg string, statusCode int) {
    j.Logger.Printf("error: %v", err)

    msg := customMsg
    if msg == "" && err != nil {
        msg = err.Error()
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

Notes

Currently tested primarily with SQLite but should be compatible with any SQL database supported by database/sql. Contributions, bug reports, and performance improvements are highly appreciated!

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidJSON  = errors.New("invalid json")
	ErrInvalidParam = errors.New("invalid param")
)

Functions

func RegisterCreate

func RegisterCreate[In Model](pattern string, mux *http.ServeMux, f func(context.Context, In) (int, error), errw ErrorWriter)

func RegisterDelete

func RegisterDelete(pattern string, mux *http.ServeMux, f func(context.Context, int) error, errw ErrorWriter)

func RegisterGet

func RegisterGet[Out Model](pattern string, mux *http.ServeMux, f func(ctx context.Context, id int) (Out, error), errw ErrorWriter)

func RegisterGetAll

func RegisterGetAll[Out any](pattern string, mux *http.ServeMux, f func(context.Context) ([]Out, error), errw ErrorWriter)

func RegisterUpdate

func RegisterUpdate[In Model](pattern string, mux *http.ServeMux, f func(ctx context.Context, in In, id int) error, errw ErrorWriter)

Types

type DefaultErrorWriter

type DefaultErrorWriter struct{}

func (DefaultErrorWriter) WriteError

func (d DefaultErrorWriter) WriteError(w http.ResponseWriter, _ *http.Request, err error, customMsg string, statusCode int)

type ErrorWriter

type ErrorWriter interface {
	WriteError(w http.ResponseWriter, r *http.Request, err error, customMsg string, statusCode int)
}

type Model

type Model interface {
	StructToMap(d interface{}) map[string]any
}

type Reflection

type Reflection struct{}

func (*Reflection) StructToMap

func (r *Reflection) StructToMap(d interface{}) map[string]any

type Repository

type Repository[M Model] struct {
	// contains filtered or unexported fields
}

func NewGenericRepository

func NewGenericRepository[M Model](db *sql.DB, table string, callback func() M) *Repository[M]

func (*Repository[M]) Create

func (r *Repository[M]) Create(ctx context.Context, model M) (int, error)

func (*Repository[M]) Delete

func (r *Repository[M]) Delete(ctx context.Context, id int) error

func (*Repository[M]) Get

func (r *Repository[M]) Get(ctx context.Context, id int) (M, error)

func (*Repository[M]) GetAll

func (r *Repository[M]) GetAll(ctx context.Context) ([]M, error)

func (*Repository[M]) GetTable

func (r *Repository[M]) GetTable() string

func (*Repository[M]) Update

func (r *Repository[M]) Update(ctx context.Context, model M, id int) error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL