logo
Liferay REST Builder vs. Hand-Rolled JAX-RS_ When to Use Which for Custom Headless APIs
#Liferay

Liferay REST Builder vs JAX-RS: Which Should You Use?

Pradip Chavda
Sep 3, 2026

Introduction

Liferay gives you two real paths to build a custom headless API on top of it, and picking the wrong one usually doesn’t hurt until six months in – when a REST Builder project outgrows generated CRUD, or a hand-rolled JAX-RS service reinvents authentication and permission-checking that Liferay already gives you for free.

Quick guide: implementers should read the comparison table and the two code walkthroughs. Architects deciding on an approach for a new project can jump straight to “When to Use Which.”

The Two Paths

REST Builder

REST Builder is Liferay’s code-generation tool. You describe your API in two files — rest-config.yaml (Liferay-specific wiring) and rest-openapi.yaml (the actual OpenAPI schema) — then run a Gradle task. It generates the JAX-RS application, resource interfaces, DTOs, and a resource-implementation package for you to fill in, plus a matching GraphQL endpoint. Critically, it also wires into Liferay’s authentication pipeline, permission checking, and OAuth2 scopes automatically.

The catch: REST Builder assumes a Service Builder module underneath it for persistence and permissions, and it’s only available on Liferay PaaS and Self-Hosted – not SaaS, since it depends on deploying custom OSGi modules.

Hand-Rolled JAX-RS

This is just a plain JAX-RS resource class, registered as an OSGi component, the same way you’d write a REST endpoint outside Liferay. No generated scaffolding, no automatic pipeline integration, you wire authentication, permissions, and serialization yourself.

Comparison

Factor REST Builder Hand-Rolled JAX-RS
Setup speed Fast – schema-first, generates boilerplate Slower – everything written by hand
Liferay auth/permissions Wired in automatically You implement it yourself
GraphQL support Generated alongside REST for free Not included – build separately
Flexibility Best for CRUD over a defined schema Full control over logic and response shape
Requires Service Builder Yes, in practice No
SaaS availability Not available Available
Upgrade safety Regenerating overwrites hand edits outside marked sections You own the whole lifecycle

REST Builder in Practice

apiDir: "api" 
apiPackagePath: "com.acme.headless.foo" 
application: 
  baseURI: "foo" 
clientDir: "client" 
implDir: "impl"

The matching OpenAPI schema fragment (rest-openapi.yaml) defining one resource:

paths: 
  /foo/{fooId}: 
    get: 
      operationId: getFoo 
      parameters: 
        - name: fooId 
          in: path 
          schema: 
            type: integer 
      responses: 
        "200": 
          content: 
            application/json: 
              schema: 
                $ref: "#/components/schemas/Foo"

After running the Gradle build task and deploying, the endpoint is live and discoverable through Liferay’s own Gogo Shell command:

jaxrs:check 
curl -u 'test@liferay.com:learn' \ 
  "http://localhost:8080/o/headless-foo/v1.0/foo/1"

Hand-Rolled JAX-RS in Practice

The equivalent, written by hand, is a plain resource class registered as an OSGi component – no generated interfaces, no rest-config.yaml:

				
					@Component( 
  property = { 
    "osgi.jaxrs.application.select=" + 
      "(osgi.jaxrs.name=Liferay.Foo)", 
    "osgi.jaxrs.resource=true" 
  }, 
  service = FooResource.class 
) 
@Path("/foo") 
public class FooResource { 
 
    @GET 
    @Path("/{fooId}") 
    public Response getFoo(@PathParam("fooId") long fooId) { 
        // your own permission check, lookup, and 
        // response mapping all belong here 
        return Response.ok(_fooService.getFoo(fooId)).build(); 
    } 
} 
				
			

Notice what’s missing compared to the generated version: there’s no automatic permission check, no OAuth2 scope wiring, and no GraphQL counterpart — all of that is your responsibility to add if you need it.

When to Use Which

Use REST Builder when:

  • The API is fundamentally CRUD over a Service Builder entity.
  • You want Liferay’s permission model and OAuth2 scopes enforced automatically.
  • You also want a GraphQL endpoint without building one separately.
  • You’re on PaaS or Self-Hosted, not SaaS.

Use hand-rolled JAX-RS when:

  • The endpoint doesn’t map cleanly to CRUD – orchestration across systems, non-standard aggregation, or a response shape that isn’t entity-based.
  • You’re on Liferay SaaS, where REST Builder’s generated OSGi modules aren’t deployable.
  • You need to bypass or heavily customize the default permission-checking flow for a specific integration.

The two aren’t mutually exclusive within one project: it’s common to use REST Builder for the standard entity endpoints and drop a hand-written JAX-RS resource alongside it for the one or two endpoints that don’t fit the generated model.

Common Mistakes

1. Editing generated code outside the marked sections

REST Builder regenerates its output on every build. Custom logic belongs in the impl module’s resource implementation, not in the generated interfaces – otherwise a rebuild silently discards it.

2. Reaching for REST Builder without a Service Builder module

REST Builder expects an entity model and persistence layer underneath it. Bolting it onto a non-entity, orchestration-style endpoint usually means fighting the generator rather than benefiting it – that’s a sign to use JAX-RS directly instead.

3. Forgetting the SaaS constraint

Planning a REST Builder-based architecture without confirming the deployment target is PaaS or Self-Hosted is one of the more expensive mistakes to discover late – verify this before committing to the approach.

Conclusion

Neither approach is universally “better” – REST Builder trades some flexibility for speed and built-in Liferay integration; hand-rolled JAX-RS trades setup speed for full control. The decision comes down to whether your endpoint is fundamentally CRUD over an entity Liferay already models well, and whether your deployment target even supports REST Builder’s custom OSGi modules in the first place.

pradip-profile-photo

Written by Pradip Chavda

A Senior Liferay Developer and Enterprise Platform Consultant with extensive experience in building scalable digital solutions using Liferay DXP. Passionate about enterprise architecture, backend engineering, low-code development, and performance optimization, the author shares practical insights to help development teams build secure, maintainable, and future-ready enterprise applications.

Delivering Engineering Excellence Across Global Markets

  • India
  • USA
  • UAE
  • Europe
  • Singapore
  • Australia

Build the Right Liferay API for Your Business

Need help designing custom headless APIs in Liferay? Our Liferay experts can help you choose the right approach and build a scalable API architecture.