Skip to content

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:

xml
<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:

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

java
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, @range included.
  • Return plain records such as Book; the runtime turns them into the shape the client asked for.
  • The author field 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

java
// 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

java
public static HttpServer start(int port, Store store) throws IOException {
    return Rayfold.http(server(store))
        .viewer(Bookshop::viewerOf)
        .explorer("Bookshop")
        .start(port);
}
sh
./mvnw compile exec:java

Open 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:

sh
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

Released under the Apache-2.0 license.