REST (Representational State Transfer) is an architectural style for building web services. In Java, we can create RESTful web services using JAX-RS (Java API for RESTful Web Services).
1. JAX-RS Basics with Jersey
Maven Dependencies
<!-- pom.xml --> <dependencies> <!-- Jersey JAX-RS implementation --> <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-binding</artifactId> <version>3.1.3</version> </dependency> <!-- For JSON processing --> <dependency> <groupId>com.fasterxml.jackson.jaxrs</groupId> <artifactId>jackson-jaxrs-json-provider</artifactId> <version>2.15.2</version> </dependency> <!-- Jakarta Servlet API --> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> </dependencies>
Web Configuration
<!-- src/main/webapp/WEB-INF/web.xml --> <?xml version="1.0" encoding="UTF-8"?> <web-app version="6.0" xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"> <servlet> <servlet-name>Jersey REST Service</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>com.example.rest</param-value> </init-param> <init-param> <param-name>jersey.config.server.provider.classnames</param-name> <param-value>org.glassfish.jersey.jackson.JacksonFeature</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey REST Service</servlet-name> <url-pattern>/api/*</url-pattern> </servlet-mapping> </web-app>
Alternative: Using Application Config Class
// REST configuration class package com.example.config; import jakarta.ws.rs.ApplicationPath; import org.glassfish.jersey.server.ResourceConfig; @ApplicationPath("/api") public class RestApplicationConfig extends ResourceConfig { public RestApplicationConfig() { // Register REST resources packages("com.example.rest.resources"); packages("com.example.rest.services"); // Register JSON support register(org.glassfish.jersey.jackson.JacksonFeature.class); // Register CORS filter register(CorsFilter.class); // Register exception mappers register(GenericExceptionMapper.class); } } 2. Basic REST Resources
Domain Models
// User entity package com.example.rest.models; import java.time.LocalDateTime; import java.util.Objects; public class User { private Long id; private String username; private String email; private String firstName; private String lastName; private LocalDateTime createdAt; private LocalDateTime updatedAt; // Constructors public User() {} public User(Long id, String username, String email, String firstName, String lastName) { this.id = id; this.username = username; this.email = email; this.firstName = firstName; this.lastName = lastName; this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id); } @Override public int hashCode() { return Objects.hash(id); } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", email='" + email + '\'' + '}'; } } // API Response wrapper package com.example.rest.models; import java.util.List; public class ApiResponse<T> { private boolean success; private String message; private T data; private List<String> errors; // Constructors public ApiResponse() {} public ApiResponse(boolean success, String message, T data) { this.success = success; this.message = message; this.data = data; } public ApiResponse(boolean success, String message) { this.success = success; this.message = message; } // Static factory methods public static <T> ApiResponse<T> success(T data) { return new ApiResponse<>(true, "Operation successful", data); } public static <T> ApiResponse<T> success(String message, T data) { return new ApiResponse<>(true, message, data); } public static <T> ApiResponse<T> error(String message) { return new ApiResponse<>(false, message); } public static <T> ApiResponse<T> error(String message, List<String> errors) { ApiResponse<T> response = new ApiResponse<>(false, message); response.setErrors(errors); return response; } // Getters and Setters public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } public List<String> getErrors() { return errors; } public void setErrors(List<String> errors) { this.errors = errors; } } Basic User Resource
package com.example.rest.resources; import com.example.rest.models.User; import com.example.rest.models.ApiResponse; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.*; @Path("/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class UserResource { // In-memory storage (replace with database in real application) private static final Map<Long, User> users = new HashMap<>(); private static long nextId = 1; static { // Initialize with some sample data users.put(1L, new User(1L, "john_doe", "[email protected]", "John", "Doe")); users.put(2L, new User(2L, "jane_smith", "[email protected]", "Jane", "Smith")); nextId = 3; } // GET /api/users - Get all users @GET public Response getAllUsers() { List<User> userList = new ArrayList<>(users.values()); ApiResponse<List<User>> response = ApiResponse.success("Users retrieved successfully", userList); return Response.ok(response).build(); } // GET /api/users/{id} - Get user by ID @GET @Path("/{id}") public Response getUserById(@PathParam("id") Long id) { User user = users.get(id); if (user == null) { ApiResponse<User> response = ApiResponse.error("User not found with id: " + id); return Response.status(Response.Status.NOT_FOUND).entity(response).build(); } ApiResponse<User> response = ApiResponse.success("User retrieved successfully", user); return Response.ok(response).build(); } // POST /api/users - Create new user @POST public Response createUser(User user) { // Validate input if (user.getUsername() == null || user.getEmail() == null) { ApiResponse<User> response = ApiResponse.error("Username and email are required"); return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); } // Check if username already exists boolean usernameExists = users.values().stream() .anyMatch(u -> u.getUsername().equals(user.getUsername())); if (usernameExists) { ApiResponse<User> response = ApiResponse.error("Username already exists"); return Response.status(Response.Status.CONFLICT).entity(response).build(); } // Create new user user.setId(nextId++); user.setCreatedAt(java.time.LocalDateTime.now()); user.setUpdatedAt(java.time.LocalDateTime.now()); users.put(user.getId(), user); ApiResponse<User> response = ApiResponse.success("User created successfully", user); return Response.status(Response.Status.CREATED).entity(response).build(); } // PUT /api/users/{id} - Update user @PUT @Path("/{id}") public Response updateUser(@PathParam("id") Long id, User updatedUser) { User existingUser = users.get(id); if (existingUser == null) { ApiResponse<User> response = ApiResponse.error("User not found with id: " + id); return Response.status(Response.Status.NOT_FOUND).entity(response).build(); } // Update fields if (updatedUser.getFirstName() != null) { existingUser.setFirstName(updatedUser.getFirstName()); } if (updatedUser.getLastName() != null) { existingUser.setLastName(updatedUser.getLastName()); } if (updatedUser.getEmail() != null) { existingUser.setEmail(updatedUser.getEmail()); } existingUser.setUpdatedAt(java.time.LocalDateTime.now()); ApiResponse<User> response = ApiResponse.success("User updated successfully", existingUser); return Response.ok(response).build(); } // DELETE /api/users/{id} - Delete user @DELETE @Path("/{id}") public Response deleteUser(@PathParam("id") Long id) { User user = users.remove(id); if (user == null) { ApiResponse<User> response = ApiResponse.error("User not found with id: " + id); return Response.status(Response.Status.NOT_FOUND).entity(response).build(); } ApiResponse<String> response = ApiResponse.success("User deleted successfully", null); return Response.ok(response).build(); } // GET /api/users/search - Search users by username @GET @Path("/search") public Response searchUsers(@QueryParam("username") String username) { if (username == null || username.trim().isEmpty()) { ApiResponse<List<User>> response = ApiResponse.error("Username parameter is required"); return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); } List<User> matchingUsers = users.values().stream() .filter(user -> user.getUsername().toLowerCase().contains(username.toLowerCase())) .toList(); ApiResponse<List<User>> response = ApiResponse.success("Search completed", matchingUsers); return Response.ok(response).build(); } } 3. Advanced REST Features
Exception Handling
// Custom exception package com.example.rest.exceptions; public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } public ResourceNotFoundException(String message, Throwable cause) { super(message, cause); } } // Exception mapper package com.example.rest.exceptions; import com.example.rest.models.ApiResponse; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; @Provider public class ResourceNotFoundExceptionMapper implements ExceptionMapper<ResourceNotFoundException> { @Override public Response toResponse(ResourceNotFoundException exception) { ApiResponse<Object> response = ApiResponse.error(exception.getMessage()); return Response.status(Response.Status.NOT_FOUND).entity(response).build(); } } // Generic exception mapper package com.example.rest.exceptions; import com.example.rest.models.ApiResponse; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; import java.util.logging.Logger; @Provider public class GenericExceptionMapper implements ExceptionMapper<Exception> { private static final Logger logger = Logger.getLogger(GenericExceptionMapper.class.getName()); @Override public Response toResponse(Exception exception) { logger.severe("Unhandled exception: " + exception.getMessage()); ApiResponse<Object> response = ApiResponse.error("Internal server error"); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(response).build(); } } // Validation exception mapper package com.example.rest.exceptions; import com.example.rest.models.ApiResponse; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; import java.util.List; import java.util.stream.Collectors; @Provider public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> { @Override public Response toResponse(ConstraintViolationException exception) { List<String> errors = exception.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.toList()); ApiResponse<Object> response = ApiResponse.error("Validation failed", errors); return Response.status(Response.Status.BAD_REQUEST).entity(response).build(); } } Validation with Bean Validation
// Validated User model package com.example.rest.models; import jakarta.validation.constraints.*; import java.time.LocalDateTime; public class ValidatedUser { private Long id; @NotBlank(message = "Username is required") @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") private String username; @NotBlank(message = "Email is required") @Email(message = "Email should be valid") private String email; @NotBlank(message = "First name is required") @Size(max = 100, message = "First name cannot exceed 100 characters") private String firstName; @NotBlank(message = "Last name is required") @Size(max = 100, message = "Last name cannot exceed 100 characters") private String lastName; private LocalDateTime createdAt; private LocalDateTime updatedAt; // Constructors, getters, and setters public ValidatedUser() {} public ValidatedUser(Long id, String username, String email, String firstName, String lastName) { this.id = id; this.username = username; this.email = email; this.firstName = firstName; this.lastName = lastName; } // Getters and setters... public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } } // Validated User Resource package com.example.rest.resources; import com.example.rest.models.ValidatedUser; import com.example.rest.models.ApiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.*; @Path("/validated-users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class ValidatedUserResource { private static final Map<Long, ValidatedUser> users = new HashMap<>(); private static long nextId = 1; @POST public Response createUser(@Valid @NotNull ValidatedUser user) { // Validation is handled by @Valid annotation user.setId(nextId++); user.setCreatedAt(java.time.LocalDateTime.now()); user.setUpdatedAt(java.time.LocalDateTime.now()); users.put(user.getId(), user); ApiResponse<ValidatedUser> response = ApiResponse.success("User created successfully", user); return Response.status(Response.Status.CREATED).entity(response).build(); } @PUT @Path("/{id}") public Response updateUser(@PathParam("id") Long id, @Valid @NotNull ValidatedUser updatedUser) { ValidatedUser existingUser = users.get(id); if (existingUser == null) { throw new ResourceNotFoundException("User not found with id: " + id); } // Update fields existingUser.setFirstName(updatedUser.getFirstName()); existingUser.setLastName(updatedUser.getLastName()); existingUser.setEmail(updatedUser.getEmail()); existingUser.setUpdatedAt(java.time.LocalDateTime.now()); ApiResponse<ValidatedUser> response = ApiResponse.success("User updated successfully", existingUser); return Response.ok(response).build(); } @GET @Path("/{id}") public Response getUserById(@PathParam("id") @Min(1) Long id) { ValidatedUser user = users.get(id); if (user == null) { throw new ResourceNotFoundException("User not found with id: " + id); } ApiResponse<ValidatedUser> response = ApiResponse.success("User retrieved successfully", user); return Response.ok(response).build(); } } Pagination and Filtering
// Pagination model package com.example.rest.models; public class PaginationRequest { private int page = 0; private int size = 10; private String sortBy = "id"; private String sortDirection = "asc"; // Constructors public PaginationRequest() {} public PaginationRequest(int page, int size, String sortBy, String sortDirection) { this.page = page; this.size = size; this.sortBy = sortBy; this.sortDirection = sortDirection; } // Getters and setters public int getPage() { return page; } public void setPage(int page) { this.page = page; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public String getSortBy() { return sortBy; } public void setSortBy(String sortBy) { this.sortBy = sortBy; } public String getSortDirection() { return sortDirection; } public void setSortDirection(String sortDirection) { this.sortDirection = sortDirection; } public int getOffset() { return page * size; } } // Paginated response package com.example.rest.models; import java.util.List; public class PaginatedResponse<T> { private List<T> content; private int page; private int size; private long totalElements; private int totalPages; // Constructors public PaginatedResponse() {} public PaginatedResponse(List<T> content, int page, int size, long totalElements) { this.content = content; this.page = page; this.size = size; this.totalElements = totalElements; this.totalPages = (int) Math.ceil((double) totalElements / size); } // Getters and setters public List<T> getContent() { return content; } public void setContent(List<T> content) { this.content = content; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public long getTotalElements() { return totalElements; } public void setTotalElements(long totalElements) { this.totalElements = totalElements; } public int getTotalPages() { return totalPages; } public void setTotalPages(int totalPages) { this.totalPages = totalPages; } public boolean isFirst() { return page == 0; } public boolean isLast() { return page >= totalPages - 1; } public boolean hasNext() { return page < totalPages - 1; } public boolean hasPrevious() { return page > 0; } } // Advanced User Resource with Pagination package com.example.rest.resources; import com.example.rest.models.*; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.util.*; import java.util.stream.Collectors; @Path("/advanced-users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class AdvancedUserResource { private static final Map<Long, User> users = new HashMap<>(); static { // Initialize with sample data for (long i = 1; i <= 50; i++) { users.put(i, new User(i, "user" + i, "user" + i + "@example.com", "FirstName" + i, "LastName" + i)); } } @GET public Response getUsers( @QueryParam("page") @DefaultValue("0") int page, @QueryParam("size") @DefaultValue("10") int size, @QueryParam("sortBy") @DefaultValue("id") String sortBy, @QueryParam("sortDir") @DefaultValue("asc") String sortDir, @QueryParam("search") String search) { // Validate pagination parameters if (page < 0) page = 0; if (size < 1 || size > 100) size = 10; List<User> userList = new ArrayList<>(users.values()); // Apply search filter if (search != null && !search.trim().isEmpty()) { userList = userList.stream() .filter(user -> user.getUsername().toLowerCase().contains(search.toLowerCase()) || user.getEmail().toLowerCase().contains(search.toLowerCase()) || user.getFirstName().toLowerCase().contains(search.toLowerCase()) || user.getLastName().toLowerCase().contains(search.toLowerCase())) .collect(Collectors.toList()); } // Apply sorting final String finalSortBy = sortBy; final String finalSortDir = sortDir; userList.sort((u1, u2) -> { int result; switch (finalSortBy) { case "username": result = u1.getUsername().compareTo(u2.getUsername()); break; case "email": result = u1.getEmail().compareTo(u2.getEmail()); break; case "firstName": result = u1.getFirstName().compareTo(u2.getFirstName()); break; case "lastName": result = u1.getLastName().compareTo(u2.getLastName()); break; default: result = u1.getId().compareTo(u2.getId()); } return "desc".equalsIgnoreCase(finalSortDir) ? -result : result; }); // Apply pagination int totalElements = userList.size(); int fromIndex = Math.min(page * size, totalElements); int toIndex = Math.min((page + 1) * size, totalElements); List<User> paginatedList = userList.subList(fromIndex, toIndex); PaginatedResponse<User> paginatedResponse = new PaginatedResponse<>( paginatedList, page, size, totalElements); ApiResponse<PaginatedResponse<User>> response = ApiResponse.success("Users retrieved successfully", paginatedResponse); return Response.ok(response).build(); } } 4. Security and CORS
CORS Filter
package com.example.rest.filters; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.ext.Provider; import java.io.IOException; @Provider public class CorsFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization, x-requested-with"); responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); responseContext.getHeaders().add("Access-Control-Max-Age", "1209600"); } } Authentication Filter
package com.example.rest.filters; import jakarta.annotation.Priority; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; import java.io.IOException; import java.util.Base64; @Provider @Priority(Priorities.AUTHENTICATION) public class AuthenticationFilter implements ContainerRequestFilter { private static final String AUTHENTICATION_SCHEME = "Bearer"; private static final String REALM = "example"; @Override public void filter(ContainerRequestContext requestContext) throws IOException { // Skip authentication for certain paths if (requestContext.getUriInfo().getPath().contains("public")) { return; } // Get the Authorization header from the request String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); // Validate the Authorization header if (!isTokenBasedAuthentication(authorizationHeader)) { abortWithUnauthorized(requestContext); return; } // Extract the token from the Authorization header String token = authorizationHeader.substring(AUTHENTICATION_SCHEME.length()).trim(); try { // Validate the token validateToken(token); } catch (Exception e) { abortWithUnauthorized(requestContext); } } private boolean isTokenBasedAuthentication(String authorizationHeader) { return authorizationHeader != null && authorizationHeader.toLowerCase().startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " "); } private void abortWithUnauthorized(ContainerRequestContext requestContext) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED) .header(HttpHeaders.WWW_AUTHENTICATE, AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"") .build()); } private void validateToken(String token) throws Exception { // In a real application, you would validate the token against your authentication service // This is a simplified example if (token == null || token.isEmpty()) { throw new Exception("Invalid token"); } // Example: Check if token is a valid JWT or session token // You would typically use a JWT library here } } 5. Testing REST Services
Unit Tests with JUnit and Jersey Test
package com.example.rest.test; import com.example.rest.models.User; import com.example.rest.models.ApiResponse; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class UserResourceTest { private static final String BASE_URL = "http://localhost:8080/api/users"; private static Client client; @BeforeAll public static void setUp() { client = ClientBuilder.newClient(); } @AfterAll public static void tearDown() { if (client != null) { client.close(); } } @Test @Order(1) public void testCreateUser() { User newUser = new User(); newUser.setUsername("testuser"); newUser.setEmail("[email protected]"); newUser.setFirstName("Test"); newUser.setLastName("User"); Response response = client.target(BASE_URL) .request(MediaType.APPLICATION_JSON) .post(Entity.entity(newUser, MediaType.APPLICATION_JSON)); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); ApiResponse<User> apiResponse = response.readEntity( new jakarta.ws.rs.core.GenericType<ApiResponse<User>>() {}); assertTrue(apiResponse.isSuccess()); assertNotNull(apiResponse.getData()); assertEquals("testuser", apiResponse.getData().getUsername()); } @Test @Order(2) public void testGetUser() { Response response = client.target(BASE_URL + "/1") .request(MediaType.APPLICATION_JSON) .get(); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); ApiResponse<User> apiResponse = response.readEntity( new jakarta.ws.rs.core.GenericType<ApiResponse<User>>() {}); assertTrue(apiResponse.isSuccess()); assertNotNull(apiResponse.getData()); } @Test @Order(3) public void testGetAllUsers() { Response response = client.target(BASE_URL) .request(MediaType.APPLICATION_JSON) .get(); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); ApiResponse<?> apiResponse = response.readEntity(ApiResponse.class); assertTrue(apiResponse.isSuccess()); } @Test @Order(4) public void testUpdateUser() { User updatedUser = new User(); updatedUser.setFirstName("Updated"); updatedUser.setLastName("Name"); updatedUser.setEmail("[email protected]"); Response response = client.target(BASE_URL + "/1") .request(MediaType.APPLICATION_JSON) .put(Entity.entity(updatedUser, MediaType.APPLICATION_JSON)); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); ApiResponse<User> apiResponse = response.readEntity( new jakarta.ws.rs.core.GenericType<ApiResponse<User>>() {}); assertTrue(apiResponse.isSuccess()); assertEquals("Updated", apiResponse.getData().getFirstName()); } @Test @Order(5) public void testDeleteUser() { Response response = client.target(BASE_URL + "/1") .request(MediaType.APPLICATION_JSON) .delete(); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); // Verify user is deleted Response getResponse = client.target(BASE_URL + "/1") .request(MediaType.APPLICATION_JSON) .get(); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), getResponse.getStatus()); } } 6. Spring Boot REST Alternative
Spring Boot REST Controller
// If using Spring Boot instead of JAX-RS package com.example.rest.controllers; import com.example.rest.models.User; import com.example.rest.models.ApiResponse; import com.example.rest.models.PaginatedResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.*; @RestController @RequestMapping("/api/users") public class UserController { private final Map<Long, User> users = new HashMap<>(); private long nextId = 1; @GetMapping public ResponseEntity<ApiResponse<List<User>>> getAllUsers() { List<User> userList = new ArrayList<>(users.values()); ApiResponse<List<User>> response = ApiResponse.success("Users retrieved successfully", userList); return ResponseEntity.ok(response); } @GetMapping("/{id}") public ResponseEntity<ApiResponse<User>> getUserById(@PathVariable Long id) { User user = users.get(id); if (user == null) { ApiResponse<User> response = ApiResponse.error("User not found with id: " + id); return ResponseEntity.status(404).body(response); } ApiResponse<User> response = ApiResponse.success("User retrieved successfully", user); return ResponseEntity.ok(response); } @PostMapping public ResponseEntity<ApiResponse<User>> createUser(@RequestBody User user) { user.setId(nextId++); user.setCreatedAt(java.time.LocalDateTime.now()); user.setUpdatedAt(java.time.LocalDateTime.now()); users.put(user.getId(), user); ApiResponse<User> response = ApiResponse.success("User created successfully", user); return ResponseEntity.status(201).body(response); } @PutMapping("/{id}") public ResponseEntity<ApiResponse<User>> updateUser(@PathVariable Long id, @RequestBody User updatedUser) { User existingUser = users.get(id); if (existingUser == null) { ApiResponse<User> response = ApiResponse.error("User not found with id: " + id); return ResponseEntity.status(404).body(response); } existingUser.setFirstName(updatedUser.getFirstName()); existingUser.setLastName(updatedUser.getLastName()); existingUser.setEmail(updatedUser.getEmail()); existingUser.setUpdatedAt(java.time.LocalDateTime.now()); ApiResponse<User> response = ApiResponse.success("User updated successfully", existingUser); return ResponseEntity.ok(response); } @DeleteMapping("/{id}") public ResponseEntity<ApiResponse<String>> deleteUser(@PathVariable Long id) { User user = users.remove(id); if (user == null) { ApiResponse<String> response = ApiResponse.error("User not found with id: " + id); return ResponseEntity.status(404).body(response); } ApiResponse<String> response = ApiResponse.success("User deleted successfully", null); return ResponseEntity.ok(response); } } Key Features Covered:
- Basic CRUD Operations - GET, POST, PUT, DELETE
- Error Handling - Custom exception mappers
- Validation - Bean Validation with custom messages
- Pagination - Pageable responses with metadata
- Filtering & Searching - Query parameter-based filtering
- Security - Authentication and CORS filters
- Testing - Comprehensive test examples
- Response Wrapping - Consistent API response format
- Content Negotiation - JSON support
This comprehensive RESTful web service implementation provides a solid foundation for building production-ready APIs in Java.