JAX-RS (RESTful Web Services)

Building REST APIs with Jakarta EE

← Back to Index

What is JAX-RS?

Think of JAX-RS like a restaurant's ordering system:

JAX-RS (Jakarta RESTful Web Services) is the standard API for building REST APIs in Jakarta EE. It uses annotations to map HTTP requests to Java methods.

Popular implementations: Jersey, RESTEasy, Apache CXF

Simple REST API Example

@Path("/hello")  // Base URL: /api/hello
public class HelloResource {

    @GET  // HTTP GET request
    @Produces(MediaType.TEXT_PLAIN)
    public String sayHello() {
        return "Hello, World!";
    }
}

// Access: GET http://localhost:8080/api/hello
// Returns: Hello, World!

Application Configuration

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")  // All endpoints start with /api
public class RestApplication extends Application {
    // No code needed! Just this annotation configures everything
}

HTTP Methods

@Path("/users")
public class UserResource {

    @Inject
    private UserService userService;

    // GET /api/users - Get all users
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    // GET /api/users/123 - Get specific user
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public User getUser(@PathParam("id") Long id) {
        return userService.findById(id);
    }

    // POST /api/users - Create new user
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createUser(@Valid User user) {
        User created = userService.create(user);
        return Response.status(Response.Status.CREATED)
                       .entity(created)
                       .build();
    }

    // PUT /api/users/123 - Update user
    @PUT
    @Path("/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public User updateUser(@PathParam("id") Long id, @Valid User user) {
        return userService.update(id, user);
    }

    // DELETE /api/users/123 - Delete user
    @DELETE
    @Path("/{id}")
    public Response deleteUser(@PathParam("id") Long id) {
        userService.delete(id);
        return Response.noContent().build();
    }
}
HTTP Method Purpose Has Body Idempotent
GET Retrieve data No Yes
POST Create resource Yes No
PUT Update (replace) Yes Yes
PATCH Partial update Yes No
DELETE Remove resource No Yes

Path Parameters

@Path("/products")
public class ProductResource {

    // Single parameter: /api/products/123
    @GET
    @Path("/{id}")
    public Product getProduct(@PathParam("id") Long id) {
        return findProduct(id);
    }

    // Multiple parameters: /api/products/electronics/laptops
    @GET
    @Path("/{category}/{subcategory}")
    public List<Product> getByCategory(
        @PathParam("category") String category,
        @PathParam("subcategory") String subcategory) {
        return findByCategory(category, subcategory);
    }
}

Query Parameters

@Path("/search")
public class SearchResource {

    // URL: /api/search?q=laptop&minPrice=500&maxPrice=2000
    @GET
    public List<Product> search(
        @QueryParam("q") String query,
        @QueryParam("minPrice") @DefaultValue("0") double minPrice,
        @QueryParam("maxPrice") @DefaultValue("10000") double maxPrice) {

        return searchProducts(query, minPrice, maxPrice);
    }

    // Pagination: /api/search?page=2&size=20
    @GET
    @Path("/paginated")
    public List<Product> getPaginated(
        @QueryParam("page") @DefaultValue("1") int page,
        @QueryParam("size") @DefaultValue("10") int size) {

        return getPage(page, size);
    }
}

Request and Response

Consuming JSON (Request Body)

@POST
@Path("/orders")
@Consumes(MediaType.APPLICATION_JSON)  // Accept JSON input
@Produces(MediaType.APPLICATION_JSON)  // Return JSON output
public Response createOrder(Order order) {
    // JAX-RS automatically converts JSON to Order object!
    Order created = orderService.create(order);

    return Response.status(201)  // HTTP 201 Created
                   .entity(created)
                   .build();
}

HTTP Status Codes

@Path("/products")
public class ProductResource {

    @GET
    @Path("/{id}")
    public Response getProduct(@PathParam("id") Long id) {
        Product product = productService.findById(id);

        if (product == null) {
            return Response.status(Response.Status.NOT_FOUND)  // 404
                           .entity("Product not found")
                           .build();
        }

        return Response.ok(product).build();  // 200 OK
    }

    @POST
    public Response create(Product product) {
        if (product.getName() == null) {
            return Response.status(Response.Status.BAD_REQUEST)  // 400
                           .entity("Name is required")
                           .build();
        }

        Product created = productService.create(product);
        return Response.status(Response.Status.CREATED).entity(created).build();  // 201
    }

    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") Long id) {
        productService.delete(id);
        return Response.noContent().build();  // 204 No Content
    }
}

Common Status Codes:

Exception Handling

WebApplicationException

@GET
@Path("/{id}")
public Product getProduct(@PathParam("id") Long id) {
    Product product = productService.findById(id);

    if (product == null) {
        // Throw exception that becomes HTTP response
        throw new WebApplicationException(
            "Product not found",
            Response.Status.NOT_FOUND
        );
    }

    return product;
}

Exception Mapper

// Custom exception
public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) {
        super(message);
    }
}

// Exception mapper
@Provider
public class ProductNotFoundMapper
    implements ExceptionMapper<ProductNotFoundException> {

    @Override
    public Response toResponse(ProductNotFoundException exception) {
        return Response.status(Response.Status.NOT_FOUND)
                       .entity(new ErrorMessage(exception.getMessage()))
                       .build();
    }
}

// Now you can throw it anywhere
@GET
@Path("/{id}")
public Product getProduct(@PathParam("id") Long id) {
    return productService.findById(id)
        .orElseThrow(() -> new ProductNotFoundException("Product not found"));
}

Filters and Interceptors

Request Filter (Authentication)

@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext requestContext) {
        String token = requestContext.getHeaderString("Authorization");

        if (token == null || !isValidToken(token)) {
            requestContext.abortWith(
                Response.status(Response.Status.UNAUTHORIZED)
                        .entity("Unauthorized")
                        .build()
            );
        }
    }

    private boolean isValidToken(String token) {
        // Validate JWT token
        return true;
    }
}

Response Filter (CORS)

@Provider
public class CorsFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext,
                      ContainerResponseContext responseContext) {
        responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
        responseContext.getHeaders().add("Access-Control-Allow-Methods",
                                         "GET, POST, PUT, DELETE");
        responseContext.getHeaders().add("Access-Control-Allow-Headers",
                                         "Content-Type, Authorization");
    }
}

Async Processing

@Path("/async")
public class AsyncResource {

    @Inject
    private ReportService reportService;

    @GET
    @Path("/report")
    public void generateReport(@Suspended AsyncResponse response) {
        // Process in background thread
        new Thread(() -> {
            try {
                // Long-running task
                String report = reportService.generate();
                response.resume(report);  // Send response when ready
            } catch (Exception e) {
                response.resume(e);  // Send error
            }
        }).start();

        // Method returns immediately, response sent later
    }
}

Content Negotiation

@Path("/products")
public class ProductResource {

    @GET
    @Path("/{id}")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Product getProduct(@PathParam("id") Long id) {
        // Returns JSON or XML based on Accept header
        // Accept: application/json → JSON response
        // Accept: application/xml → XML response
        return productService.findById(id);
    }

    @POST
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response create(Product product) {
        // Accepts JSON or XML based on Content-Type header
        return Response.ok(productService.create(product)).build();
    }
}

Best Practices

✅ DO:

  • Use proper HTTP methods - GET for read, POST for create, etc.
  • Return correct status codes - 200, 201, 404, 500, etc.
  • Use @Valid for validation - Automatic bean validation
  • Handle exceptions properly - Use exception mappers
  • Version your APIs - /api/v1/users, /api/v2/users
  • Use pagination for large datasets - page and size parameters
  • Document your API - OpenAPI/Swagger
  • Secure endpoints - Authentication and authorization

❌ DON'T:

  • Don't use GET for mutations - Use POST/PUT/DELETE
  • Don't return null - Return 404 or empty list
  • Don't expose entities directly - Use DTOs
  • Don't forget CORS - Frontend needs it
  • Don't ignore status codes - Always return appropriate codes
  • Don't put business logic in resources - Use services
  • Don't forget error handling - Always handle exceptions

Summary

  • JAX-RS builds REST APIs with annotations
  • @Path: Maps URL to resource class/method
  • @GET, @POST, @PUT, @DELETE: HTTP methods
  • @PathParam: URL path variables (/users/{id})
  • @QueryParam: Query string parameters (?page=1)
  • @Produces/@Consumes: Content type (JSON, XML)
  • Response: Build custom HTTP responses with status codes
  • Exception handling: ExceptionMapper for custom error responses
  • Filters: Request/response interceptors (auth, CORS)
  • Implementations: Jersey, RESTEasy, Apache CXF