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.