JAX-RS: Java API for RESTful Web Services
JAX-RS (Java API for RESTful Web Services) is a specification that simplifies the development of REST APIs in Java. It provides a set of annotations and interfaces that let developers expose plain Java objects as web resources with minimal boilerplate. Popular implementations include Jersey (the reference implementation), RESTEasy, and Apache CXF.
In this post, we’ll explore the core concepts of JAX-RS and build a practical example.
Why JAX-RS?
Before JAX-RS, building REST services in Java often meant working directly with servlets and manually parsing requests. JAX-RS abstracts away this complexity by:
- Mapping HTTP methods to Java methods via annotations
- Handling content negotiation automatically
- Serializing and deserializing objects (JSON, XML)
- Providing clean URI templating and parameter injection
Setting Up Dependencies
If you’re using Maven with Jersey, add the following to your pom.xml:
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>3.1.3</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode
Core Annotations
JAX-RS revolves around a handful of key annotations:
Annotation Purpose@Path
Defines the URI path for a resource
@GET, @POST, @PUT, @DELETE
Bind methods to HTTP verbs
@Produces
Declares the media type returned
@Consumes
Declares the media type accepted
@PathParam
Injects a value from the URI path
@QueryParam
Injects a query string parameter
Building a Resource Class
Let’s create a simple resource for managing books:
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.*;
@Path("/books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
private static final Map<Integer, Book> BOOKS = new HashMap<>();
private static int counter = 1;
@GET
public Collection<Book> getAllBooks() {
return BOOKS.values();
}
@GET
@Path("/{id}")
public Response getBook(@PathParam("id") int id) {
Book book = BOOKS.get(id);
if (book == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(book).build();
}
@POST
public Response createBook(Book book) {
int id = counter++;
book.setId(id);
BOOKS.put(id, book);
return Response.status(Response.Status.CREATED).entity(book).build();
}
@DELETE
@Path("/{id}")
public Response deleteBook(@PathParam("id") int id) {
if (BOOKS.remove(id) == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.noContent().build();
}
}
Enter fullscreen mode Exit fullscreen mode
The Book class is a plain POJO:
public class Book {
private int id;
private String title;
private String author;
// getters and setters omitted for brevity
}
Enter fullscreen mode Exit fullscreen mode
Registering the Application
To bootstrap your JAX-RS application, extend Application and register your resources:
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import java.util.Set;
@ApplicationPath("/api")
public class RestApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(BookResource.class);
}
}
Enter fullscreen mode Exit fullscreen mode
With this configuration, your endpoints are available under /api/books.
Working with Query Parameters
Query parameters are useful for filtering and pagination:
@GET
@Path("/search")
public Collection<Book> searchBooks(
@QueryParam("author") String author,
@QueryParam("limit") @DefaultValue("10") int limit) {
return BOOKS.values().stream()
.filter(b -> author == null || author.equals(b.getAuthor()))
.limit(limit)
.toList();
}
Enter fullscreen mode Exit fullscreen mode
The @DefaultValue annotation ensures a sensible fallback when the parameter is absent.
Exception Handling
Rather than scattering try-catch blocks everywhere, use an ExceptionMapper to centralize error handling:
java
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable ex) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", ex.getMessage()))
.build();
}
Enter fullscreen mode Exit fullscreen mode