crabster

Crabster and the others

{% raw %} What tools of the same kind do, what they solved that Crabster has not, and what they decided the other way. Written to arbitrate — not to sell.

What this page rests on. The claims about JHipster were checked against its documentation and issue tracker, listed at the end. The ones that were not are marked (unverified). JHipster moves fast: re-check before relying on a detail.

The ones about Crabster were checked against the repository, by running it, on 16 September 2026. It is dated because this page has already been wrong in the dumbest direction: for several days it advertised three gaps the code had closed in the meantime. A comparison page ages both ways.

And the bias is obvious: this page is written from inside Crabster. What follows tries to compensate by being harder on Crabster than on the others, because Crabster is the only one of the two whose faults cost us anything.

Four families, and only one has the merge problem🔗

The classification that explains everything else: what becomes of the generated code?

FamilyExamplesWhat regenerating means
1. Generate and disownrails g scaffold, Spring Initializr, cargo newNothing: there is no regeneration. The code is yours the second it is written
2. Generate a part nobody touchessqlc, Prisma Client, OpenAPI GeneratorPainless, because the boundary is physical: the generated code lives in files you do not edit
3. Generate nothingHasura, PostgREST, SupabaseMoot: the API is derived from the schema at runtime
4. Generate a whole project meant to be editedJHipster, CrabsterA merge problem, and it is the price of the promise

Family 4 is the only one promising "a complete application and it is your code". Merging is what that promise costs — not an implementation defect that could be fixed by working harder.

Families 2 and 3 are worth looking at before choosing 4. If what you need is "a CRUD API over a schema", Hasura or PostgREST give you one without a line of code, and the price — you do not own the engine, arbitrary logic does not go in it — is not always the wrong trade.

JHipster🔗

Ten years old, a large ecosystem, frontends, microservices, dozens of database and framework options. Crabster is a young single-author project. Any comparison that forgot that would be dishonest.

The architecture is nevertheless strikingly close — Crabster follows the same pattern:

JHipsterCrabster
JDL (entity, relationship, enum, application)CDL (record, ref, enum, service)
.yo-rc.json + .jhipster/*.json.crabster/project.toml + model.cdl
Needles — jhipster-needle-* markers the generator injects atSlots — _into/<file>/<slot>.tera
Blueprints (generator-jhipster-* on npm)Blueprints (directories of modules)
jhipster upgradecrabster upgrade — same intent, different mechanism (below)

Needles and slots are the same idea: a named injection point in a file another generator owns. So are blueprints, more mature on their side — a JHipster blueprint can replace a whole sub-generator, not just a template.

What JHipster solved, and where Crabster stands🔗

This section used to list four gaps. Three are closed since, the fourth half — and what is genuinely still ahead is none of the four.

JHipsterCrabster
Related objects in responsesMapStruct DTOs carry the related objectClosed. ?expand=customer,product replaces the identifier with the record, at one query per reference for the whole page rather than one per row. One level, not a tree: what is expanded carries the identifier of the next one
Rich filtersequals, notEquals, specified, in, contains, greaterThan, lessThan… generated over JPA CriteriaClosed, except in. The same operators, picked from the column's type — contains on text, the four comparisons on what orders, specified on what is optional. And a misspelt parameter gets a 400 naming the ones that exist, where ignoring it would return the whole table — which looks like a result
Searchsearch with elasticsearch: a full integrationClosed. search meilisearch gives every record a full-text endpoint, on tables as on documents: what is indexed is the document the API returns, so the module never asks where records are kept
Audit columnsAbstractAuditingEntity — createdBy, createdDate, lastModifiedBy, lastModifiedDate (unverified)Half. @audited gives createdAt and updatedAt, written by the code as it writes. createdBy and lastModifiedBy need an identity, and so a link between the authentication module and the record one that nothing ties today

What is still ahead cannot be caught up by writing code: ten years, an ecosystem and people. Dozens of blueprints published by somebody other than the generator's own team, a user base that reports defects before they reach everyone, and an answer to needs this repository has not met yet — distributed cache, messaging, sending mail, i18n of the generated application, an admin screen for accounts (unverified). Crabster has none of that, and above all it still has no users at all.

What neither has solved🔗

What the two decided differently🔗

JHipsterCrabster
Development database ≠ productiondevDatabaseType h2Disk / prodDatabaseType postgresqlRefused. One database per project
CRUD securityClosed by default, opened afterwardsOpen by default, closed by naming the extractor
FrontendGenerated (Angular, React, Vue), in coreNo UI application in core, by decision. A typed client — TypeScript and Rust — generated from the model; UI applications as blueprints, and three of them exist — React, Vue, Angular — written against that client and shipped in the binary. The difference is not "with or without an interface" but who carries the cost of a UI framework when it breaks (ADR-0003)

The first deserves a word, because it is the one place where I think Crabster is right against established practice: the schema is not the same from one database to the next. A Timestamp is a DATETIME on MySQL and a timestamptz elsewhere; a Decimal does not round-trip on SQLite. Testing on H2 and deploying on PostgreSQL means passing your tests against a schema production never has — and JHipster issues about "works on H2, breaks after" are not rare. The need behind the option is real; Crabster answers it with testcontainers against the database actually targeted.

jhipster upgrade: their answer to the family-4 problem🔗

It is clever and blunt. The command:

  1. creates or checks out an orphan jhipster_upgrade branch;
  2. regenerates the whole application there (--force --with-entities) with the new generator version, and commits;
  3. merges that branch into yours with git merge.

Conflicts are raised by git and resolved by you. On a lightly customised application it is painless; on one where you wrote a lot, it is a conflict session with a reputation.

crabster upgrade: the same intent, a different mechanism🔗

Crabster does the same thing, with one piece of information JHipster does not have: it knows which files you touched, because it stamped them as it wrote them. The command regenerates from .crabster/, then decides file by file — it rewrites what is still exactly as it wrote it, leaves what you edited untouched and puts its own version in .crabster/incoming/, and deletes what it no longer produces if you never touched it.

On a file you edited, the merge is a real three-way merge, like theirs. The difference is the common ancestor: JHipster has git produce one from a regenerated orphan branch; Crabster keeps its own, in .crabster/snapshot/, a copy of every file as it wrote it.

JHipsterCrabster
What arbitratesgit mergea three-way merge against the copy the tool kept
What tells your work apartnothing: git compares two textsthe recorded stamp — the tool knows who wrote each file
Requires gityes, and an orphan branchno
On a conflictmarkers inside your filesyour files intact, the marked merge left beside them
What it costs the repositorynothing extraevery generated file is committed twice

Those last two rows are the real trade. Crabster will not plant markers in a file it did not write — a project outside git would then have no way back — and it pays for that independence from git with a committed copy of the generated code.

This is ADR-0002 carried out as written. It does rule JHipster's mechanism out explicitly:

Alternative ruled out: full regeneration with manual git diff left to the user — simple to implement, but shifts the entire cost onto the user and does not scale to a real project.

One piece of history is worth keeping: this section said, until recently, that JHipster had an imperfect answer and Crabster none. The reasoning that kept pushing crabster upgrade back was wrong — it assumed a snapshot was a prerequisite for the command, when the stamp already answered the only question that blocked it. The snapshot came afterwards, and all it adds is the merge.

Where Crabster is ahead🔗

Four things, and they should be said as honestly as the rest.

It knows what you touched. Every generated file carries a stamp. JHipster regenerates everything and lets git decide — it cannot tell your work from its own. Crabster refuses to overwrite and names the file, on record and on upgrade alike, and that is also what lets it merge only where there is something to merge.

The error contract. RFC 9457 for every failure, with a type the project serves itself, and an OpenAPI document generated from the handlers — so it cannot drift from the code. That is tighter than the genre's average.

Optimistic locking. JHipster does not generate @Version: the issues asking for it have been open since 2015, and until recently this was a shared gap — both tools let two concurrent writes overwrite each other in silence. @versioned closes it: an ETag on reads, a required If-Match on writes, 412 for a version since passed, 428 for a write that says nothing, and the check in the same SQL statement as the write.

No support library. The generated project depends on no Crabster runtime: no io.github.jhipster, nothing. Walking away from the tool costs nothing.

What I would take from them🔗

This list was the catching-up list. It is empty, and that is itself a result worth dating: all four entries are done.

  1. Related objects in responses — done, ?expand=.
  2. The filter operators — done, except in, which is waiting for somebody who needs it rather than for one more row in a table.
  3. A fallback answer to version bumps, however ugly — moot: the real three-way merge arrived before the ugly answer was ever written.
  4. Optimistic locking — done, as @versioned. It was somewhere to get ahead rather than to catch up, and it is.

So what is left to take from them is not a feature, it is a piece of ecosystem machinery: a blueprint able to replace a whole sub-generator, not just a set of templates. As long as nobody writes a blueprint outside this repository the question stays theoretical — and that is exactly what phase 18 measures.

And what I would not take: devDatabaseType.

Sources🔗