pREST v2.0.0-rc6 — Hexagonal Architecture and the Adapter Refactor

pREST v2.0.0-rc6 splits one large Adapter interface into nine port interfaces. Each HTTP handler now takes only the ports it needs. A unit test can mock one port without a live database.

pREST is the open-source tool that turns PostgreSQL into a REST API. Below are the result, the problem it solved and the refactor step by step. Binaries and Docker images are on the v2.0.0-rc6 release page.

Update: pREST v2.0.0 GA and v2.1.0 are released. For multi-database support and MCP, see the v2.0.0 / v2.1.0 overview and MCP tutorial. For the prest-mcp adapter and Cursor/OpenClaw plugins, see AI plugins. This post covers the rc6 architecture refactor. For GitHub delivery metrics, see DeliveryCompass.

Table of Contents

What the refactor gets you

pREST exposes PostgreSQL tables and custom SQL scripts over HTTP. It’s a low-code REST layer for existing or new Postgres applications. Version 2 is a major internal overhaul shipped as release candidates. v2.0.0-rc6 is the latest RC, not final GA.

The headline change is architectural. Handlers depend on narrow port interfaces, and controllers are dependency-injected structs. Here’s what that buys:

  • gomock and sqlmock unit tests run per handler. Integration tests run in a separate CI workflow against Docker Compose Postgres.
  • Dedicated test-unit.yml and test-integration.yml workflows replace a single combined test job.
  • A new database backend implements only the port interfaces its handlers need. The HTTP layer stays unchanged.
  • bcrypt replaces MD5 as the default password encryption algorithm in v2 config.

The architecture work landed in PR #968. The security fixes, query features and build tooling in this release land on that cleaner foundation.

Why one big Adapter had to go

Before v2, pREST’s database layer was a single large Adapter interface. That one type owned catalog queries (databases, schemas, tables), request-to-SQL building and query execution. It also owned permissions checks, script running and transaction management.

Controllers made the coupling worse. Package-level functions reached into global config and the global adapter instance. A handler that only ran a SELECT still lived in a package tied to the full adapter surface.

That cost the project in three places:

  • Mocks had to satisfy the entire Adapter interface, even when a test exercised one code path.
  • Edits to catalog SQL building risked touching CRUD or script paths, because everything shared one interface boundary.
  • A new database backend had to implement every method on day one. Nothing could depend on a subset.

Step 1: split the Adapter into ports

v2 applies hexagonal architecture (ports and adapters). The application core, the HTTP handlers, depends on narrow port interfaces. Concrete implementations such as the Postgres adapter sit outside as driven adapters.

flowchart TB
    subgraph driving [Driving adapters]
        HTTP[HTTP controllers]
    end
    subgraph core [Application core]
        Handlers[AuthHandler CatalogHandler CRUDHandler ...]
        Ports[Port interfaces]
    end
    subgraph driven [Driven adapters]
        PG[postgres.Adapter]
    end
    HTTP --> Handlers
    Handlers --> Ports
    Ports --> PG

The monolithic Adapter is split into focused interfaces in adapters/:

Port Responsibility
CatalogQuerier SQL for listing databases, schemas, and tables
RequestQueryBuilder Build queries from HTTP request parameters
QueryExecutor Execute queries and return result scanners
SQLBuilder Construct INSERT/UPDATE/DELETE/SELECT statements
PermissionsChecker Table- and field-level access control
ScriptRunner Execute custom SQL scripts
DatabaseRegistry Resolve database names and connections
TransactionManager Begin and manage transactions
LegacyExecutor Non-context execution with transaction support

Step 2: keep a composite Adapter

The composite Adapter interface still exists, so existing wiring stays compatible. It embeds all port interfaces. The Postgres implementation satisfies one type, and callers that need the full surface keep using Adapter:

// adapters/adapter.go
type Adapter interface {
    RequestQueryBuilder
    QueryExecutor
    CatalogQuerier
    SQLBuilder
    PermissionsChecker
    ScriptRunner
    DatabaseRegistry
    TransactionManager
    LegacyExecutor
}

Handlers depend only on the ports they need. CatalogHandler takes a CatalogQuerier. CRUDHandler takes QueryExecutor, PermissionsChecker and related ports, never the full adapter.

Step 3: turn controllers into handler structs

HTTP controllers in controllers/ are now structs with injected dependencies. These are the driving adapters: AuthHandler, CatalogHandler, CRUDHandler, TableHandler, ScriptHandler and HealthHandler. The router registers methods on these structs instead of package-level functions.

Step 4: wire the ports at startup

Dependency assembly lives in controllers/deps.go. A Deps struct holds typed port references:

type Deps struct {
    Catalog  adapters.CatalogQuerier
    Builder  adapters.RequestQueryBuilder
    Executor adapters.QueryExecutor
    SQL      adapters.SQLBuilder
    Perms    adapters.PermissionsChecker
    Scripts  adapters.ScriptRunner
    DB       adapters.DatabaseRegistry
    Cache    ResponseCacher
    // ...
}

NewDepsFromConfig maps a single Postgres Adapter instance into each port field. The same concrete object is passed several times, narrowed to different interface types. NewHandlers then builds each handler with only its required deps.

Middleware follows the same pattern. AccessControl and CRUDStack receive a PermissionsChecker through injection instead of reaching for global state.

Request flow: a CRUD read

A GET /{database}/{schema}/{table} request flows like this:

  1. The router dispatches to the CRUD middleware stack.
  2. CRUDStack middleware calls PermissionsChecker.TablePermissions to verify the user can read the table.
  3. CRUDHandler.Select calls PermissionsChecker.FieldsPermissions for column-level access. It builds SQL via the query builder and executes through QueryExecutor.QueryCtx.
  4. JSON bytes go back to the client, optionally cached via ResponseCacher.

Each step depends on an interface, not a global. Unit tests can inject gomock implementations per port without a live database.

What else ships in rc6

These are the other highlights from the changelog:

  • A JWT auth bypass fix (#960). With JWT enforcement on and no signing key configured, requests could bypass authentication. A misconfiguration now fails closed.
  • OR clause support (#958). Filter queries can express OR conditions, for clients that build filters from HTTP parameters.
  • GoReleaser Docker images (#953). Official container images are built through GoReleaser, in line with the binary builds.
  • Structured logging (#950). The API layer moves to Go’s slog for structured, leveled logging.
  • Go 1.26 and split unit and integration CI.

How to try v2.0.0-rc6

This is a release candidate. Pin the version and watch the prest/prest repo for GA.

With Docker:

docker pull prest/prest:v2.0.0-rc6

For binaries, download platform-specific builds from the v2.0.0-rc6 release assets.

For issues, feedback or migration questions, open a discussion or issue on GitHub. The v2.0.0-rc6 changelog links to every merged PR.

The service-boundary and deployment lessons behind this layout are in Go microservices in production.

FAQ

What is pREST v2.0.0-rc6?

It’s the latest release candidate for pREST 2.0, an open-source Go tool that exposes PostgreSQL as a REST API. It brings a major architectural refactor: hexagonal design, Adapter decomposition and dependency-injected controllers. It also adds security fixes, OR clause support, GoReleaser Docker images and structured logging via slog.

What is the Adapter interface refactor?

The v2 refactor splits pREST’s monolithic Adapter interface into nine smaller port interfaces, such as CatalogQuerier, QueryExecutor and RequestQueryBuilder. A composite Adapter still embeds all ports, so the Postgres implementation satisfies one type. HTTP handlers depend only on the ports they need. That improves testability and makes future database backends easier to add.

What is hexagonal architecture in the context of pREST?

HTTP controllers (driving adapters) depend on port interfaces in the application core. The Postgres adapter (driven adapter) implements those ports. Dependencies point inward, so handlers never import Postgres-specific code. Wiring happens at startup through NewDepsFromConfig and NewHandlers.

Is v2.0.0-rc6 production-ready?

It’s a release candidate, not a final GA release. It suits evaluation and staging, where teams can pin the version and give feedback. For production, wait for a stable v2.0.0 GA. Or accept RC-level risk with a pinned image tag and a rollback plan.