openxava / documentation / Automatic REST API generation

All the features in this article are available only in XavaPro

Table of contents

Enabling the API
Interactive documentation (Swagger UI)
Authentication and security
Obtaining a JWT token
JWT secret configuration
Role and module permissions
CRUD endpoints
Endpoint summary
URL pluralization
Retrieving a list
Retrieving by ID
Creating an entity
Updating an entity
Deleting an entity
Pagination
Sorting
Filtering
Associations and element collections
Tab base conditions
HTTP errors
Audit trail and email notifications
Multitenancy
CORS configuration
Production checklist
Client examples

Since v8.0 XavaPro automatically generates and publishes a REST API for all your JPA entities. You do not need to write controllers, DTOs, mappers, or configuration files: just write your domain entities with standard JPA and OpenXava annotations, and the REST API is immediately available.

The generated API includes:

Enabling the API

By default, the automatic API is turned off for security. You can easily activate it from the administration user interface without restarting the server.

Go to the Admin folder in your application menu and open the Configuration module. In the configuration form, check the Auto generate API checkbox and click Save:

Auto generate API in Configuration

Once checked, all endpoints under the /api/ path become active immediately. If unchecked, any incoming request to /api/* receives an HTTP 404 response with a JSON body {"error":"API is disabled"}.

Interactive documentation (Swagger UI)

XavaPro includes an embedded, offline Swagger UI explorer that allows you to inspect and interactively test every available endpoint directly from your browser.

To access Swagger UI, open the following URL in your browser:

http://localhost:8080/yourapp/api/

Swagger UI in OpenXava

From the Swagger UI interface, you can click the Authorize button, paste the JWT (without the Bearer prefix; Swagger UI adds it), explore the JSON schemas of your entities, and execute queries directly against your application.

The raw OpenAPI 3.0 specification in JSON format is available at:

GET /api/openapi.json

Security note: /api/ and /api/openapi.json are public: they do not require a JWT and they describe every module that maps to an entity. Treat that as part of your attack surface in production (restrict network access, or keep the API disabled until you need it).

Authentication and security

Obtaining a JWT token

All REST API endpoints (except /api/login, /api/, and /api/openapi.json) are protected and require a JSON Web Token (JWT) passed in the Authorization HTTP header.

To authenticate, send a POST request to /api/login with the XavaPro user credentials:

curl -X POST http://localhost:8080/yourapp/api/login \
  -H "Content-Type: application/json" \
  -d '{"user": "admin", "password": "adminpassword"}'

If the credentials are valid and the user account is active, the server returns an HTTP 200 response with the token:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

The token is valid for 24 hours. After it expires, /api/login must be called again. Include the token in all subsequent requests using the Bearer scheme:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT secret configuration

Tokens are cryptographically signed using the HMAC-SHA256 algorithm. For production environments, configure the secret key by setting the XAVA_JWT_SECRET environment variable on your server:

export XAVA_JWT_SECRET="YourSuperSecretKeyMustBeAtLeast32CharactersLong!"

Note: If the XAVA_JWT_SECRET variable is not set or has fewer than 32 characters, OpenXava generates an ephemeral random secret key on application startup. In that case, existing tokens will be invalidated whenever the server restarts.

Role and module permissions

The automatic API strictly enforces the same security model defined in XavaPro for the web interface:

You can create a dedicated api role (or multiple API roles) in the Roles module specifically for API consumers. Assigning this role to the API user accounts lets you fine-tune exactly which modules, actions, and fields are accessible through the REST API.

CRUD endpoints

Endpoint summary

HTTP Method Endpoint Description Success Status
POST /api/login Authenticate user and obtain JWT token 200 OK
GET /api/openapi.json OpenAPI 3.0 specification (public) 200 OK
GET /api/ Interactive Swagger UI explorer (public) 200 OK
GET /api/{entity} List records (with pagination, sorting, and filtering) 200 OK
GET /api/{entity}/{id} Get a single record by primary key 200 OK
POST /api/{entity} Create a new record 201 Created
PUT /api/{entity}/{id} Update the fields sent in the body (partial update) 200 OK
DELETE /api/{entity}/{id} Delete a record 204 No Content

URL pluralization

The path segment is the entity name in lower case, pluralized with simple English heuristics: Customer becomes /api/customers, Country becomes /api/countries, Invoice becomes /api/invoices. Names that already end in s (except ss) are left as-is after lowercasing. If a generated URL surprises you, check Swagger UI or /api/openapi.json for the exact path.

Retrieving a list

Sends a GET request to the plural entity URL:

curl -X GET "http://localhost:8080/yourapp/api/customers" \
  -H "Authorization: Bearer $TOKEN"

Response:

[
  {
    "number": 1,
    "name": "ACME Corporation",
    "creditLimit": 15000.00,
    "address": {
      "street": "Main Street 123",
      "city": "Valencia",
      "zipCode": 46001
    }
  },
  {
    "number": 2,
    "name": "Global Tech Ltd",
    "creditLimit": 25000.00,
    "address": {
      "street": "Market Square 4",
      "city": "Madrid",
      "zipCode": 28001
    }
  }
]

Retrieving by ID

Sends a GET request with the primary key in the path:

curl -X GET "http://localhost:8080/yourapp/api/customers/1" \
  -H "Authorization: Bearer $TOKEN"

For entities with a composite primary key, separate the key values with commas in the order of property declaration. For an entity whose key properties are year and number:

curl -X GET "http://localhost:8080/yourapp/api/warehouseorders/2026,45" \
  -H "Authorization: Bearer $TOKEN"

Use the real key of the entity. An Invoice that is identified by a UUID oid is retrieved as /api/invoices/{oid}, not as year,number.

Creating an entity

Sends a POST request with the entity data as a JSON payload in the request body:

curl -X POST "http://localhost:8080/yourapp/api/customers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "number": 105,
    "name": "Innovate Software Inc",
    "creditLimit": 50000.00,
    "address": {
      "street": "Tech Park Ave 12",
      "city": "Barcelona",
      "zipCode": 8001
    }
  }'

The server returns HTTP 201 Created and the complete JSON representation of the created entity.

To assign a @ManyToOne reference, send only its primary-key fields. For an invoice whose customer is identified by number:

curl -X POST "http://localhost:8080/yourapp/api/invoices" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "year": 2026,
    "number": 1,
    "customer": { "number": 1 }
  }'

Updating an entity

Sends a PUT request specifying the primary key in the URL and the fields to change in the JSON body. Only the properties present in the body are updated; omitted fields keep their current values (a partial update, not a full replace):

curl -X PUT "http://localhost:8080/yourapp/api/customers/105" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Innovate Software Group",
    "creditLimit": 75000.00
  }'

The server returns HTTP 200 OK along with the updated entity.

Deleting an entity

Sends a DELETE request with the entity ID:

curl -X DELETE "http://localhost:8080/yourapp/api/customers/105" \
  -H "Authorization: Bearer $TOKEN"

Upon successful deletion, the server responds with HTTP 204 No Content.

Pagination

All list endpoints support pagination through two query parameters:

curl -X GET "http://localhost:8080/yourapp/api/customers?page=2&size=10" \
  -H "Authorization: Bearer $TOKEN"

Pagination metadata is communicated through HTTP response headers:

Example response headers:

X-Total-Count: 45
Link: <http://localhost:8080/yourapp/api/customers?page=1&size=10>; rel="first", <http://localhost:8080/yourapp/api/customers?page=1&size=10>; rel="prev", <http://localhost:8080/yourapp/api/customers?page=3&size=10>; rel="next", <http://localhost:8080/yourapp/api/customers?page=5&size=10>; rel="last"

Sorting

Use the sort query parameter to order query results by one or more properties:

curl -X GET "http://localhost:8080/yourapp/api/customers?sort=address.city,-name" \
  -H "Authorization: Bearer $TOKEN"

An unknown sort field returns HTTP 400.

Filtering

Query results can be filtered by specifying query parameters that combine the property name with an explicit operator suffix: {field}_{operator}={value}.

Operator Description Example
_eq Equal to number_eq=100
_ne Not equal to type_ne=PROSPECT
_gt Greater than creditLimit_gt=5000
_gte Greater than or equal to creditLimit_gte=5000
_lt Less than creditLimit_lt=10000
_lte Less than or equal to creditLimit_lte=10000
_like Pattern matching on strings (* for wildcard, ? for single character) name_like=ACME*
_in In a comma-separated list of values number_in=1,5,22
_null Check if null (true for IS NULL, false for IS NOT NULL) remarks_null=true

Filters can be combined (they are evaluated with logical AND) and can traverse relationships using dotted paths:

curl -X GET "http://localhost:8080/yourapp/api/customers?address.city_like=Val*&creditLimit_gte=10000&sort=-creditLimit" \
  -H "Authorization: Bearer $TOKEN"

Associations and element collections

The automatic API serializes references and embedded collections naturally into JSON:

Audit trail and email notifications

Operations performed through the REST API seamlessly trigger existing OpenXava business behaviors:

Multitenancy

XavaPro's multi-organization architecture is fully supported by the automatic REST API. Each organization has its own isolated API namespace:

Resource URL Pattern
Organization Login POST /api/orgs/{organization}/login
Organization Swagger UI GET /api/orgs/{organization}/
Organization OpenAPI Spec GET /api/orgs/{organization}/openapi.json
Organization Entity CRUD /api/orgs/{organization}/{entity}

Authenticate against the organization and then call that same namespace. The JWT does not carry the organization: the tenant is selected by the URL. Use a token obtained from /api/orgs/{organization}/login against that organization.

Example request to query customers belonging to organization acme_corp:

curl -X GET "http://localhost:8080/yourapp/api/orgs/acme_corp/customers" \
  -H "Authorization: Bearer $TOKEN"

CORS configuration

To enable web applications hosted on other domains (such as a separate React, Angular, or Vue frontend) to call your API, you can configure Cross-Origin Resource Sharing (CORS).

In the Admin > Configuration module, set the Api allowed origins property:

API allowed origins in Configuration

You can specify:

The server automatically handles OPTIONS preflight requests and adds the necessary CORS response headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Expose-Headers for X-Total-Count and Link).

Production checklist

Client examples

Here is a complete JavaScript example using fetch to authenticate, fetch a paginated list of customers with filtering, and create a new record:

const API_BASE = 'http://localhost:8080/yourapp/api';

async function main() {
  // 1. Authenticate and obtain JWT
  const loginRes = await fetch(`${API_BASE}/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ user: 'admin', password: 'adminpassword' })
  });
  const { token } = await loginRes.json();

  // 2. Query customers with filtering and pagination
  const listRes = await fetch(
    `${API_BASE}/customers?address.city_like=Val*&creditLimit_gte=5000&sort=-creditLimit&page=1&size=10`,
    {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    }
  );
  
  const totalCount = listRes.headers.get('X-Total-Count');
  const customers = await listRes.json();
  console.log(`Found ${totalCount} customers:`, customers);

  // 3. Create a new customer
  const createRes = await fetch(`${API_BASE}/customers`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      number: 201,
      name: 'New Horizon SL',
      creditLimit: 30000,
      address: {
        street: 'Avenida del Puerto 45',
        city: 'Valencia',
        zipCode: 46021
      }
    })
  });

  const created = await createRes.json();
  console.log('Customer created successfully:', created);
}

main().catch(console.error);

See also