Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Soro

Soro is an opinionated Go application framework for building production REST APIs quickly, combining convention-driven development with idiomatic Go.

Soro is developed by DataSoro. Its developer experience is inspired by the productivity of Rails API applications, while its implementation keeps normal Go structs, interfaces, generics, context.Context, explicit dependencies, PostgreSQL, and Bun available to application code.

Status: pre-release. Phases 1 through 5 are implemented; the public API is not stable.

Implemented foundation

The current foundation includes:

  • a typed application container and strict layered configuration;
  • one shared pgx pool bridged to Bun and River;
  • UUIDv7 IDs, UTC timestamps, actor fields, and JSONB metadata;
  • typed generic repositories and Bun escape hatches;
  • transactional create, update, soft delete, restore, and force delete;
  • joined nested transactions with outermost AfterCommit/AfterRollback behavior;
  • all required optional model hooks plus deterministic global and registered hooks;
  • persisted-state dirty tracking;
  • contextual and declarative validation with normalized errors;
  • readable PostgreSQL migrations and partial unique indexes;
  • PostgreSQL integration tests and a compiling example.
  • Huma-backed versioned routing, OpenAPI 3.1, and API documentation;
  • typed serializers and generic REST resources with explicit input mappers;
  • pagination, allowlisted filtering, literal ILIKE search, and sorting;
  • standard error envelopes, server-generated request IDs, and safe panic recovery;
  • resource authorization/callback/scope hooks and route introspection.
  • River-backed typed jobs sharing Bun's pool and transaction;
  • SMTP, console, and capture mail with transaction-safe SendLater;
  • structured HTTP/job/mail logging and W3C trace propagation;
  • OpenTelemetry tracing, OTLP HTTP export, and Prometheus metrics;
  • /health, /ready, and /metrics infrastructure endpoints;
  • configured HTTP timeouts and graceful server/worker shutdown.
  • the Cobra-based soro CLI with runtime, database, job, and OpenAPI commands;
  • conflict-safe application, model, resource, migration, serializer, validator, job, and mailer generators;
  • runtime-discovered PostgreSQL SQL migrations with explicit Up/Down sections;
  • generated application and PostgreSQL migration acceptance tests.
  • schema-isolated test applications, typed factories, HTTP helpers, captured mail assertions, and synchronous job-handler tests.

Phase 5 includes schema-isolated test applications, typed factories, development logging and secret redaction, generator customization, benchmark baselines, expanded examples, and the pre-v1 compatibility/release policy.

CLI quick start

Build the pre-release CLI from this checkout:

mise exec -- go install ./cmd/soro

Until Soro has a tagged module release, point a generated application at this checkout:

soro new customer-api --module example.com/customer-api --soro-replace /path/to/soro
cd customer-api
soro generate resource User \
  email:string:unique:index \
  first_name:string \
  last_name:string \
  active:bool:default=true
soro db create
soro db migrate
soro server

The resource generator writes a model, migration, serializer, input validators, CRUD resource, registration, and tests. Generated SQL uses UUID primary keys, JSONB metadata, TIMESTAMPTZ, soft deletion, and partial unique indexes. See CLI and generators.

Requirements

  • Go 1.26+
  • PostgreSQL 17+ for integration tests and the example

The repository pins Go 1.26.6 through mise.toml.

Model and repository

type User struct {
	model.Base
	Email  string `bun:"email,notnull" validate:"required,email"`
	Active bool   `bun:"active,notnull,default:true"`
}

func (u *User) BeforeCreate(ctx context.Context, lc *lifecycle.Context) error {
	u.Email = strings.ToLower(strings.TrimSpace(u.Email))
	return nil
}

func (u *User) AfterUpdate(ctx context.Context, lc *lifecycle.Context) error {
	if lc.Changes.Changed("Email") {
		oldEmail, newEmail, _ := lc.Changes.Values("Email")
		_ = oldEmail
		_ = newEmail
	}
	return nil
}
users := repository.New[User](app.DB)
user := &User{
	Base:  model.Base{Name: "Dustin"},
	Email: "USER@EXAMPLE.COM",
}

if err := users.Create(ctx, user); err != nil { /* handle */ }
found, err := users.Find(ctx, user.ID)
if err != nil { /* handle */ }
found.Active = true
if err := users.Update(ctx, found); err != nil { /* handle */ }
if err := users.Delete(ctx, found.ID); err != nil { /* soft delete */ }

deleted, err := users.OnlyDeleted().Find(ctx, found.ID)
if err != nil { /* handle */ }
if err := users.Restore(ctx, deleted.ID); err != nil { /* handle */ }
if err := users.ForceDelete(ctx, deleted.ID); err != nil { /* explicit physical delete */ }

Normal reads exclude deleted rows. WithDeleted() includes both states, and OnlyDeleted() returns deleted rows. Scope methods return repository copies and do not mutate shared state.

HTTP resources

Application input, model, and response types remain separate. The compiling basic example configures a user resource with explicit mapping and serialization:

users, err := basic.NewUserResource(repository.New[basic.User](app.DB))
if err != nil { /* handle */ }

err = app.API.Version("v1", func(v1 *api.Router) {
	if err := v1.Resource("/users", users); err != nil { /* handle */ }
})

This registers:

GET    /api/v1/users
GET    /api/v1/users/{id}
POST   /api/v1/users
PATCH  /api/v1/users/{id}
DELETE /api/v1/users/{id}

DELETE is a soft delete. OpenAPI is served at /openapi.json and /openapi.yaml, with interactive documentation at /docs. List resources accept page, per_page, search, allowlisted filter[...] parameters, and sort fields configured by the resource.

Transactions

Repository methods join a Soro transaction carried by the callback context:

err := users.Transaction(ctx, func(txCtx context.Context, txUsers *repository.Repository[User]) error {
	if err := txUsers.Create(txCtx, user); err != nil {
		return err
	}
	return txUsers.Create(txCtx, anotherUser)
})

Nested calls join the outer SQL transaction. A nested error marks the outer transaction rollback-only, even if an intermediate callback catches it. Phase 1 does not implement savepoints. AfterCommit executes only after the outer commit succeeds; an error from it is returned after data has committed and cannot roll the transaction back.

Jobs and mail

Job arguments use a stable kind and ordinary JSON fields:

type SendWelcomeEmail struct {
	UserID uuid.UUID `json:"user_id" river:"unique"`
}

func (SendWelcomeEmail) Kind() string { return "send_welcome_email" }

err := jobs.Register(app.Jobs, func(ctx context.Context, args SendWelcomeEmail) error {
	return sendWelcome(ctx, args.UserID)
})

Enqueue normally or inside the current Soro transaction:

_, err := app.Jobs.Enqueue(ctx, SendWelcomeEmail{UserID: user.ID},
	jobs.Queue("mailers"), jobs.Priority(2), jobs.UniqueByArgs())

When ctx carries a Soro transaction, Enqueue automatically uses River's transactional insertion. EnqueueTx is available when transactional context must be required explicitly.

Mail delivery is immediate or queued:

delivery := app.Mailer.Delivery(&mail.Message{
	To: []string{user.Email}, Subject: "Welcome", Text: "Hello",
})
err = delivery.Send(ctx)
_, err = delivery.SendLater(ctx, jobs.Delay(5*time.Minute))

Configuration

Configuration precedence is:

framework defaults
config/application.yaml
config/{SORO_ENV}.yaml
environment variables

Supported variables include SORO_ENV, SORO_APP_NAME, SORO_APP_VERSION, DATABASE_URL, SORO_LOG_LEVEL, SORO_LOG_FORMAT, HTTP timeout variables, SORO_JOBS_*, SORO_MAIL_*, SMTP_*, SORO_OTEL_ENABLED, OTEL_EXPORTER_OTLP_ENDPOINT, and the database pool variables. Unknown YAML fields fail startup. Production requires DATABASE_URL and SMTP mail configuration. The default logger uses readable text in development, JSON in production, and redacts standard secret-bearing fields. See configuration.

Run the example

Start PostgreSQL using any local installation or the checked-in Compose service:

docker compose up -d postgres

In another shell:

export DATABASE_URL='postgres://postgres:postgres@localhost:5432/soro?sslmode=disable'
mise exec -- go run ./examples/basic/cmd/demo

The persistence demonstration applies its migrations, idempotently seeds an Account/User/Project relationship graph, creates and updates a user, soft-deletes it, restores it, and explicitly force-deletes it. Run the HTTP example instead with:

mise exec -- go run ./examples/basic/cmd/server

Then open http://localhost:8080/docs or call the /api/v1/accounts, /api/v1/users, and /api/v1/projects resources. The example demonstrates explicit UUID relationships, metadata, factories/seeds, lifecycle changes, allowlisted filtering/search/sorting, transactional jobs, captured or SMTP mail, tracing, and metrics.

Set SORO_JOBS_ENABLED=true to work the example's transactionally enqueued welcome-mail jobs in the server process. Generated applications can run a dedicated worker with soro jobs work.

Tests

Unit tests do not require external services. PostgreSQL integration tests use schema isolation and run when SORO_TEST_DATABASE_URL is present:

mise exec -- go test ./...

SORO_TEST_DATABASE_URL='postgres://postgres:postgres@localhost:5432/soro_test?sslmode=disable' \
  mise exec -- go test ./...

SORO_TEST_DATABASE_URL='postgres://postgres:postgres@localhost:5432/soro_test?sslmode=disable' \
  mise exec -- go test -race ./...

CI always supplies PostgreSQL, so integration tests cannot silently skip there. CI also generates an aggregate coverage profile and enforces a 70% statement floor.

Design documents

License

Apache License 2.0. See LICENSE.

About

Soro is an opinionated Go application framework for building production REST APIs quickly, combining convention-driven development with idiomatic Go.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages