DEV Community

vbilopav
vbilopav

Posted on

I Spent Two Years Deleting My Backend. This Is What's Left

What happens when you stop writing controllers, services, repositories and mappers - and let PostgreSQL be the backend. MIT-licensed, and yes, I built it.


Full disclosure right away: I built the thing I'm about to show you. It's called NpgsqlRest, it's MIT-licensed, there is no paid tier, no telemetry, no "book a demo" button. I'm just a guy who spent two years deleting layers from his stack and now wants to show someone the hole where the backend used to be.

The standard pattern

The standard data access pattern for modern business applications looks like this:

UIFetchControllerServiceRepositoryORMSQLDatabase

Seven arrows. And if you look closely at what most of those layers actually do - they take data from one side and pass it to the other side, slightly renamed. The Controller maps the request to a DTO. The Service passes it to the Repository. The Repository asks the ORM nicely. The ORM generates SQL that you then inspect in a log because you don't trust it (correctly).

We built entire careers on maintaining this pipeline. I know because I did, for decades.

But once you realize database-aware tests are trivial to wire up, you can drop the Repository. Once you get good at SQL, you can drop the ORM. And then you look at the Controller and realize it's just boring glue code that ships bytes between HTTP and the database. Glue code can be automated. So:

UIDatabase

That's it. That's the architecture.

Show me or it didn't happen

Fine. This is a file called users.sql. Not a function, not a stored procedure - a plain SQL file sitting in your repo:

/*
HTTP GET /users/
@authorize admin, user
@cached
@cache_expires_in 30sec
@timeout 5min
@param $1 department_id text
*/
select id, name, email, role
from users
where $1 is null or department_id = $1;
Enter fullscreen mode Exit fullscreen mode

You run npgsqlrest (a single native executable, no runtime to install) pointed at your PostgreSQL, and:

$ curl -s 'localhost:8080/users/?department_id=1' | jq
[
  { "id": 1, "name": "Alice", "email": "alice@acme.io", "role": "admin" },
  { "id": 4, "name": "Diana", "email": "diana@acme.io", "role": "admin" },
  { "id": 7, "name": "Eve",   "email": "eve@acme.io",   "role": "user"  }
]
Enter fullscreen mode Exit fullscreen mode

The comment annotations are the whole trick. @authorize restricts the endpoint to roles. @cached gives you response caching with expiration. There are annotations for rate limiting, retries, timeouts, uploads, server-sent events, and @mcp - which exposes the same endpoint as a tool for AI agents, same auth, same limits. Everything is declared next to the SQL it belongs to, not in some middleware file three folders away.

And no, this is not one of those "compose your query in the URL" tools. There are no ?filter=eq.something operators. The API surface is exactly the SQL you wrote - joins, CTEs, window functions, whatever - and nothing else. You can audit your entire API with grep.

Where do the types go?

This is the part that sold it to me, myself and I. PostgreSQL has a rich type system, and that type system flows outward. NpgsqlRest generates a typed TypeScript client (and Dart, if Flutter is your thing) from the same metadata:

interface IUsersRequest {
    department_id?: string | null;
}

interface IUsersResponse {
    id: number | null;
    name: string | null;
    email: string | null;
    role: string | null;
}

export async function users(
    request: IUsersRequest
) : Promise<ApiResult<IUsersResponse[]>> {
    // fetch call, generated, boring, correct
}
Enter fullscreen mode Exit fullscreen mode

Change the SQL, the client regenerates, tsc catches everything that broke. The entire class of "backend changed, frontend found out in production" bugs simply cannot happen, because there is no hand-written contract to drift.

Tests are SQL files too

You test the endpoints with... more SQL files. npgsqlrest --test invokes the real endpoint pipeline in-process, on the test's own transaction - insert fixtures, call the endpoint (it sees your uncommitted rows), assert, roll back:

-- tests/get_users.test.sql
begin;

insert into users (email) values ('fixture@example.com');

/*
GET /api/get-users
# @claim user_id=1
*/
select status = 200, 'authenticated caller gets 200' from _response;
select body::jsonb @> '[{"email": "fixture@example.com"}]', 'fixture is listed' from _response;

rollback;
Enter fullscreen mode Exit fullscreen mode

No mocks, no test framework, no running server. It even reports endpoint coverage.

And the dev loop closes with npgsqlrest --watch: it restarts on SQL file changes, on config changes, and - this is the part I like - on database routine changes. You create or replace a function in psql and the endpoint is live seconds later, TypeScript client already regenerated. The live database schema becomes your type checker.

"But is it fast" and other reasonable questions

It's a compiled AOT executable. In an independent-methodology benchmark of 20 services it placed #2 with 3,500+ req/s on 4 cores - ahead of the Go and Rust frameworks in that round, which honestly surprised me too.

Auth is built in: cookies, JWT, OAuth2 flows, per-user rate limits. And unlike the row-level-security approach some similar tools push you into, authorization here is per-endpoint annotations - you don't have to encode your permission model into RLS policies.

I also shipped a real production app this way: ~74 endpoints, 12K lines of SQL, zero lines of C# or Python. Not a demo, an actual product with actual users. The case study has the honest numbers, including the parts that were annoying.

The boring enterprise stuff (it has that too)

"Cute demo" - I hear you say - "but I have requirements." Fine, here is the checklist nobody puts in the hero section: multi-host failover and read-replica routing, load balancing, connection and command retries, partitioned rate limiting, cache profiles with conditional When-rules, thread-pool tuning... even Excel and HTML response rendering, because someone in accounting will always ask for the spreadsheet. All of it configured in JSON - you don't write custom middleware for any of this.

And since it's 2026 and everything must be AI-something: the same endpoint catalog also comes out as OpenAI/Anthropic tool schemas and llms.txt, and @mcp makes any endpoint callable by AI agents. Same auth, same rate limits - the agent doesn't get a backdoor, it gets an endpoint.

Who is this for

If your idea of a good time is seven layers of Clean Architecture, this tool will upset you, and that's ok - we can still be friends. But if you've ever looked at a controller that does nothing but forward a request to a query and thought "I typed all of this for what exactly?" - then maybe give it fifteen minutes:

It's free, it's MIT, and I'll be in the comments taking both questions and abuse.

Top comments (0)