← Back to list

Build Spring Boot CRUD APIs Faster with Compile-Time Generation for JPA Entities

One annotation on a JPA entity, and the repository, DTOs, mapper, controller, and security wiring get generated at build time — as plain…

nikhil goyal · 2026-05-28 14:21 · 0 claps · 3.8 min read
#crud #spring-boot #jpa #sql #vibe-coding
Open on Medium ↗
Wiki topics: 💻 · Programming

Build Spring Boot CRUD APIs Faster with Compile-Time Generation for JPA Entities

One annotation on a JPA entity, and the repository, DTOs, mapper, controller, and security wiring get generated at build time — as plain Java, you can read and debug.

If your Spring Boot service has more than a couple of SQL entities, you already know the rhythm by heart.

Define the entity. Write the repository. Add a response DTO. Add a request DTO that’s almost the same but not quite. Wire up a MapStruct mapper. Write the controller. Sprinkle in validation, OpenAPI annotations, and security rules. Then do the whole thing again for the next table.

The code isn’t hard. That’s the frustrating part. It’s just repetitive, noisy, and easy to make subtly inconsistent across entities — one controller returns the wrong status code, another forgets a validation annotation, a third has security rules that don’t quite match the others.

That’s the itch I built [**spring-xpose](https://github.com/notablogger/spring-xpose) to scratch. With version 3.0.0, you put a single @ExposeEntity annotation on a JPA entity and the entire REST layer gets generated at compile time** — as real .java files you can open, read, and step through with a debugger.

This post is the SQL/JPA story. (There’s a companion post for MongoDB if your service leans on documents instead.)

What you get from one annotation

From a single @ExposeEntity on a JPA entity, spring-xpose generates:

One annotation, 6 classes

One annotation, 6 classes

All of it lands as plain Java under build/generated/sources/..., so you can inspect and debug it exactly like code you wrote by hand. Nothing happens at runtime through hidden reflection or proxies — the generation is done by the time the build finishes.

Why compile time, and why it helps real teams

For SQL-heavy backends, generating this layer at build time pays off in three concrete ways.

  • Boilerplate becomes configuration.
  • Consistency comes for free.
  • Iteration stays safe.

And because the output is just files on disk, you keep the thing runtime-magic approaches take away: breakpoints land in real source, and stack traces point at line numbers you can actually open.

One entity, a full API

Here’s a real example — an Order resource with Basic auth and a read/write role split:

@Entity
@ExposeEntity(
    path = "orders",
    expose = {
        Operation.FIND_ALL,
        Operation.FIND_BY_ID,
        Operation.CREATE,
        Operation.UPDATE
    },
    authType = AuthType.BASIC,
    readRoles = {"CUSTOMER", "ADMIN"},
    writeRoles = {"ADMIN"}
)
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @NotBlank
    private String reference;
    @PositiveOrZero
    private Double totalAmount;
    private String status;
}

Because this is SQL/JPA, the generated repository is a JpaRepository<Order, Long> and the generated write endpoints are transactional. You didn't write a controller, a DTO, or a security config — but you have all three.

Configurability that actually matters in production

@ExposeEntity isn't a blunt on/off switch for CRUD. You tune behavior per entity:

**expose** — which CRUD operations are generated (FIND_ALL, FIND_BY_ID, CREATE, UPDATE, DELETE).

**authType** — the security model: NONE (public), BASIC (HTTP Basic), or OAUTH2 (JWT resource server).

**readRoles / writeRoles** — role-based read vs write access, e.g. readRoles = {"USER","ADMIN"}, writeRoles = {"ADMIN"}.

**ignoredFields** — hides fields from both the request and response DTOs.

**pageable** - enables a paginated findAll list endpoint.

**relationMode** — how related entities serialize in responses: IDS_FOR_LIST_OBJECT_FOR_SINGLE, ALWAYS_IDS, or ALWAYS_OBJECT.

The last one is the lever for balancing payload size against readability in SQL relation graphs.

Security is scoped per resource

For /api/orders, the generated security config applies only to that path:

  • GET requires CUSTOMER or ADMIN
  • POST / PUT require ADMIN

So instead of one giant hand-written security file that everyone’s afraid to touch, access rules live right next to the entity they protect.

Sample App: ready to explore out of the box

The sample app is built to be cloned and run as-is. Hibernate manages the relational schema directly from your entities, and data.sql seeds demo data for category, product, orders, and report — So every endpoint has real data behind it the moment the app boots.

That makes it easy to explore what the library can do without any setup: you can immediately try out the generated endpoints, the per-resource security, and the validation behaviour against live data.

Steps to run -

The sample app spins up with Docker for the datastores and a local Spring profile:

cd spring-xpose-sample-rest
docker compose up -d postgres mongo
./gradlew bootRun --args='--spring.profiles.active=local'

Then, exercise the generated Order endpoints:

curl -i http://localhost:8080/api/orders
curl -i -u customer:customer123 http://localhost:8080/api/orders
curl -i -u admin:admin123 -X POST http://localhost:8080/api/orders \
  -H "Content-Type: application/json" \
  -d '{"reference":"ORD-NEW","totalAmount":49.99,"status":"NEW"}'

In the sample’s configuration, an unauthenticated GET /api/orders is blocked with a 403, while authenticated customer access returns 200 — the security rules straight from the annotation, working end to end.

Where this fits — and where it doesn’t

Good fit: data-centric services with many entities, internal platforms, admin APIs, and teams that value consistency and speed over handcrafting every CRUD endpoint.

Less good fit: highly custom orchestration endpoints, or domain workflows where each endpoint is unique by design. There, you’d fight the generator more than it helps.

Final take

For SQL/JPA services, spring-xpose turns the repetitive CRUD layer into compile-time configuration while staying transparent and debuggable. You still control which operations exist, who can access them, how DTOs look, and how relations are represented.

That’s the whole point: less repetitive coding, more intentional API design.

If your service also stores documents in MongoDB, the same idea applies with a Mongo-native annotation — that’s the companion post on @ExposeDocument.

Links


메타데이터
post_id
3f66e154ba3a
slug
build-spring-boot-sql-crud-apis-faster-with-compile-time-generation-3f66e154ba3a
url
https://medium.com/@notabloggerr/build-spring-boot-sql-crud-apis-faster-with-compile-time-generation-3f66e154ba3a
canonical_url
https://medium.com/@notabloggerr/build-spring-boot-sql-crud-apis-faster-with-compile-time-generation-3f66e154ba3a
author_url
https://medium.com/@notabloggerr
status
ok
fetched_at
2026-06-11 17:15:47