What is JAX-RS?
Think of JAX-RS like a restaurant's ordering system:
- 📱 Customer (Frontend) makes requests via menu (API endpoints)
- 📋 Waiter (JAX-RS) takes orders and delivers food (data)
- 🍳 Kitchen (Backend) prepares the order (business logic)
- ✨ Everything communicated in simple format (JSON)
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:
- 200 OK: Success
- 201 Created: Resource created
- 204 No Content: Success, no body
- 400 Bad Request: Invalid input
- 401 Unauthorized: Not authenticated
- 403 Forbidden: Not authorized
- 404 Not Found: Resource doesn't exist
- 500 Internal Server Error: Server error
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