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-mcpadapter 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
- Why one big Adapter had to go
- Step 1: split the Adapter into ports
- Step 2: keep a composite Adapter
- Step 3: turn controllers into handler structs
- Step 4: wire the ports at startup
- What else ships in rc6
- How to try v2.0.0-rc6
- FAQ
- Links
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.ymlandtest-integration.ymlworkflows 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
Adapterinterface, 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:
- The router dispatches to the CRUD middleware stack.
CRUDStackmiddleware callsPermissionsChecker.TablePermissionsto verify the user can read the table.CRUDHandler.SelectcallsPermissionsChecker.FieldsPermissionsfor column-level access. It builds SQL via the query builder and executes throughQueryExecutor.QueryCtx.- 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
slogfor 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.