Get started with Java
A bookshop server with four operations, in plain Java with no framework. The finished project is examples/java. You need JDK 21. Using Spring Boot? The Spring Boot guide builds the same server with annotated beans.
1. Create the project
A Maven project with one dependency:
<dependencies>
<dependency>
<groupId>dev.rayfold</groupId>
<artifactId>rayfold-java</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>2. Describe the API
Save the schema as src/main/resources/bookshop.rayfold:
entity Author {
id: ID
name: String
}
entity Book @cache(maxAge: 60s, scope: public) {
id: ID
title: String
stock: Int
author: Author
"""What the shop paid. Only staff can see it."""
costPrice: Decimal? @allow(read: viewer.role == "staff")
}
error OutOfStock { bookId: ID, available: Int }
event StockChanged { bookId: ID, stock: Int }
query book(id: ID): Book?
query books(page: PageArgs = { first: 20 }): Page<Book>
"""Take copies off the shelf. Fails if there are not enough."""
command buy(bookId: ID, qty: Int = 1 @range(min: 1, max: 10)): Book
throws OutOfStock
emits StockChanged
@allow(write: viewer != null)
command restock(bookId: ID, qty: Int @range(min: 1, max: 1000)): Book
emits StockChanged
@allow(write: viewer.role == "staff")The server reads it at startup and checks every request against it. npx rayfold gen java generates records from it if you want them.
3. Write the resolvers
public static RayfoldServer server(Store store) {
return Rayfold.server(schema())
.query("book", (args, ctx) -> store.book(args.getString("id")).orElse(null))
.query("books", (args, ctx) -> page(store.books(), args.getValues("page")))
.command("buy", (args, ctx) -> {
String bookId = args.getString("bookId");
int qty = args.getInt("qty");
Book book = store.update(bookId, current -> {
if (qty > current.stock()) {
throw Rayfold.domainError("OutOfStock",
Map.of("bookId", current.id(), "available", current.stock()),
"Only " + current.stock() + " left of " + current.title());
}
return current.withStock(current.stock() - qty);
}).orElseThrow(() -> notFound(bookId));
return stockChanged(book);
})
.command("restock", (args, ctx) -> {
String bookId = args.getString("bookId");
int qty = args.getInt("qty");
Book book = store.update(bookId, current -> current.withStock(current.stock() + qty))
.orElseThrow(() -> notFound(bookId));
return stockChanged(book);
})
// called once per level with every book in the result, and answered with one lookup
.field("Book", "author", (books, args, ctx) ->
store.authors(books.stream().map(book -> book.getString("authorId")).toList()))
.build();
}Rayfold.server(schema)takes one function per operation, named as in the schema. Arguments arrive already checked against it,@rangeincluded.- Return plain records such as
Book; the runtime turns them into the shape the client asked for. - The
authorfield loader receives every book in the result at once and returns their authors in the same order, so a page of 50 books looks up authors once. Rayfold.domainError("OutOfStock", ...)is the error the schema declared. Clients receive it by name, with its data.Rayfold.result(book).emit(...)returns the changed book, which becomes a patch for every client cache, and the event the schema says the command emits.
4. Say who is calling
// Two fixed tokens stand in for real authentication.
private static final Map<String, Map<String, String>> DEMO_VIEWERS = Map.of(
"Bearer customer", Map.of("id", "u1", "role", "customer"),
"Bearer staff", Map.of("id", "s1", "role", "staff"));
/** The viewer the schema's policies see as {@code viewer}, or null for an anonymous request. */
static Map<String, String> viewerOf(HttpExchange exchange) {
String authorization = exchange.getRequestHeaders().getFirst("Authorization");
return authorization == null ? null : DEMO_VIEWERS.get(authorization);
}What this returns is viewer in the schema's @allow rules. The resolvers never check permissions themselves.
5. Start the server
public static HttpServer start(int port, Store store) throws IOException {
return Rayfold.http(server(store))
.viewer(Bookshop::viewerOf)
.explorer("Bookshop")
.start(port);
}./mvnw compile exec:javaOpen http://localhost:4000/rayfold/explorer to browse the operations and send requests; paste Bearer customer or Bearer staff as the token to try the commands. Or call it with curl:
curl -s localhost:4000/rayfold -H 'content-type: application/rayfold+json' -H 'authorization: Bearer customer' \
-d '{"rayfold":"0.1","ops":[{"id":1,"op":"buy","args":{"bookId":"b1"},"key":"first-purchase-0001"}]}'6. Call it
Any Rayfold client works with this server: the TypeScript client, the React hooks, or the Kotlin client, which Java code can use too.
Next
- How shapes, loaders and pages work: Queries and shapes.
- Rules in the schema: Who can do what.
- Generated records and the rest of the Java API: the Java and Spring Boot guide.