Concepts — what we’re actually building#
This is the no-jargon tour. Whether you’re a product owner trying to understand what the team is building, a Java developer new to the frontend, or a React developer new to Spring, the vocabulary below is all you need to make sense of the rest of the guide.
The 30-second version#
A Palmyra app has four moving parts:
- A model — a Java class that describes one kind of record (a Department, an Employee).
- A handler — a small Java class that says “publish this model as an HTTP endpoint.”
- A store — a JavaScript helper that knows how to call that endpoint from the browser.
- A grid or form — a React component that reads from a store and puts rows on the screen.
You tag the Java classes with a few annotations; Palmyra fills in the SQL, the URL routing, the paging, the JSON parsing, and the HTTP calls. You write the business logic and the screens — not the plumbing.
The players, in plain language#
Model#
Imagine a single index card describing an Employee: email, first name, last name, which department they’re in, when they joined, whether they’re active. A model in Palmyra is that index card, written as a Java class. Annotations on the class (@PalmyraType, @PalmyraField) tell Palmyra which table the card maps to and what to do with each field — is it searchable, is it required, is it the primary key, is it sensitive and should never leave the server?
Every request and response on the API is shaped like this card in JSON form.
Handler#
A handler is the object that says “here’s a URL; the records behind it are shaped like that model.” Palmyra ships different kinds of handlers — one for reading lists, one for reading single records, one for creating, one for updating, one for upserting, one for deleting. You pick the ones you need and the framework does the rest.
A single line of annotation (@CrudMapping(mapping = "/department", type = DepartmentModel.class, …)) wires a handler to its URL.
The JSON contract#
When the browser asks the server for a list of departments, the response looks like this:
{
"result": [
{ "id": 1, "code": "HR", "name": "Human Resources" },
{ "id": 2, "code": "ENG", "name": "Engineering" }
],
"limit": 15,
"offset": 0,
"total": 2
}Every Palmyra list endpoint returns this shape. Every single-record endpoint returns the inner object. Every POST body carries an object in the same shape. The frontend and the backend never have to agree on anything beyond this.
Store#
On the frontend, a store is the bit of code that knows how to talk to one endpoint. You ask the factory for the Department store once; after that, fetching a page, saving an edit, and deleting a row are one-line calls.
You don’t write a fetch wrapper per entity. One factory creates them all.
Grid#
A grid is the table you see on the screen. You tell it which columns to show and which endpoint to read from; the grid handles paging, sorting, searching, and the refresh after a save. In this guide we use SummaryGrid — a ready-made component that takes a few inputs and draws the whole list screen for you.
Form#
A form collects input — text boxes, dropdowns, date pickers — and sends it to the server. Palmyra ships three flavours:
- New form — for creating a record.
- Edit form — for modifying an existing record (loads the current values on open).
- View form — read-only display of a single record.
Each field you put inside a form declares an attribute — the name it’ll use in the JSON it sends to the server. That name matches the model field it maps to. That single string (attribute) is the only place the frontend and the backend explicitly share a name.
How the pieces fit, for one feature#
Here’s the full trip for “show me a list of employees in Engineering”:
- The user clicks Employees in the sidebar.
- The Mantine grid asks its store for rows filtered to
department.code = "ENG". - The store sends
GET /api/employee?department.code=ENG&_limit=15&_offset=0. - The handler for that URL inspects the filter, adds any security rules the operator doesn’t need to know about, and asks Palmyra to read the rows.
- Palmyra generates the SQL — including the JOIN from
employeetodepartment— runs it, and turns the result into the standard JSON envelope. - The store unwraps the envelope, hands the rows to the grid.
- The grid draws the table.
Nobody wrote SQL. Nobody wrote an axios call. The pieces match up because the model names the columns, the handler names the URL, and the form / grid field names its attribute to match.
What you’ll actually write#
After the tutorial, the codebase you’ve built has:
- Two Java model classes (Department, Employee) — about 30 lines each.
- Two Java handlers — one-liners plus a couple of overrides for defaults.
- One store factory config — shared across every frontend page.
- Ten React files — five per entity (grid, new-form, view page, view form, edit form).
That’s the whole application. No SQL in .sql files (except the table definition), no hand-written REST controllers, no per-entity fetch wrappers, no custom form-state library on top of React.
Glossary#
| Term | What it is |
|---|---|
@PalmyraType |
Annotation that maps a POJO to a database table |
@PalmyraField |
Annotation that configures one attribute on a model (column, searchable, required, etc.) |
@CrudMapping |
Annotation that binds a handler to its URL(s) and model |
QueryHandler |
Interface for read-many / list endpoints |
ReadHandler |
Interface for read-one endpoints |
SaveHandler |
Interface for upsert endpoints — insert when new, update when existing |
PalmyraStoreFactory |
Frontend class that creates one store per endpoint |
SummaryGrid |
Ready-made React grid component |
DialogNewForm / DialogEditForm |
Ready-made modal form components |
MantineServerLookup |
Dropdown that queries a server endpoint for its options — used for foreign keys |
attribute |
The field name used by both the model and the form — the single cross-stack coupling |
endPoint |
The URL path the frontend uses to name one Palmyra resource |
With the vocabulary in place, the Backend track starts from an empty Gradle project.