DEV Community

Said Olano
Said Olano

Posted on

JAX-RS: Building RESTful Web Services in Java (2026-08-20 22:15)

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

Top comments (0)