Jersey is the reference implementation of the Jakarta RESTful Web Services (formerly JAX‑RS) specification.
Jersey 3.x targets Jakarta EE 9+, which completed the rename of all javax.* packages to jakarta.*.
This is a breaking change from Jersey 2.x – any code using javax.ws.rs must be updated to jakarta.ws.rs.
This guide builds a working REST API from scratch using Jersey 3 with the JDK's built‑in HTTP server, requiring no application server.
The javax → jakarta Migration
Jakarta EE 9 (released 2020) renamed all javax.* packages to jakarta.*.
The key imports that change for REST development are:
If you are migrating an existing Jersey 2 application, you'll need to:
- Update all imports from
javax.ws.rstojakarta.ws.rs. - Update your Maven/Gradle dependencies to Jersey 3.x.
- Update your application server to Jakarta EE 9+ if you deploy to one.
Maven Dependencies
Here's the complete pom.xml dependency set for Jersey 3 with the JDK HTTP container and Jackson JSON support:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<jersey.version>3.1.5</jersey.version>
</properties>
<dependencies>
<!-- Jersey core server -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- JDK HTTP server container -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-jdk-http</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- Dependency injection support -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- JSON support via Jackson -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>
</dependencies>
Note: The jersey-hk2 dependency provides HK2 (a lightweight dependency injection framework) that Jersey uses for internal wiring.
You may also use CDI if you're in a Jakarta EE container, but for a standalone app, HK2 is the default.
Application Class
The Application subclass is the entry point for JAX‑RS configuration. It tells Jersey which resource classes and providers to register:
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import java.util.Set;
@ApplicationPath("/api")
public class MyApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(
MenuResource.class
);
}
}
The @ApplicationPath annotation sets the base URI path for all resources. All endpoints defined in MenuResource will be available under /api/....
You can also use ResourceConfig (a Jersey subclass) for programmatic registration, which we'll see later.
Resource Class
The resource class defines the REST endpoints. Notice the use of jakarta.ws.rs.* annotations:
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Collection;
@Path("/menu")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MenuResource {
private final MenuService service = new MenuService();
@GET
public Collection<MenuItem> getAll() {
return service.findAll();
}
@GET
@Path("/{id}")
public Response getById(@PathParam("id") int id) {
MenuItem item = service.findById(id);
if (item == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(item).build();
}
@POST
public Response create(MenuItem item) {
MenuItem created = service.save(item);
return Response.status(Response.Status.CREATED).entity(created).build();
}
@PUT
@Path("/{id}")
public Response update(@PathParam("id") int id, MenuItem item) {
item.setId(id);
MenuItem updated = service.update(item);
return Response.ok(updated).build();
}
@DELETE
@Path("/{id}")
public Response delete(@PathParam("id") int id) {
service.delete(id);
return Response.noContent().build();
}
}
Notice the use of Response for fine‑grained control over HTTP status codes and headers.
The @Produces and @Consumes at the class level apply to all methods unless overridden.
Model and Service Classes
MenuItem.java
public class MenuItem {
private int id;
private String name;
private double price;
public MenuItem() {}
public MenuItem(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// getters and setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
MenuService.java – In‑memory store
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class MenuService {
private final Map<Integer, MenuItem> store = new HashMap<>();
private final AtomicInteger idGen = new AtomicInteger(1);
public MenuService() {
save(new MenuItem(0, "Burger", 8.99));
save(new MenuItem(0, "Pizza", 11.49));
save(new MenuItem(0, "Salad", 6.99));
}
public Collection<MenuItem> findAll() { return store.values(); }
public MenuItem findById(int id) { return store.get(id); }
public MenuItem save(MenuItem item) {
int id = idGen.getAndIncrement();
item.setId(id);
store.put(id, item);
return item;
}
public MenuItem update(MenuItem item) {
store.put(item.getId(), item);
return item;
}
public void delete(int id) { store.remove(id); }
}
The service is simple and not thread‑safe – for a real application, you'd use a proper database, but this serves the purpose of the example.
Main Class: Starting the Server
The main class uses JdkHttpServerFactory to create an HTTP server that delegates requests to Jersey:
import com.sun.net.httpserver.HttpServer;
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import java.net.URI;
public class Main {
private static final URI BASE_URI = URI.create("http://localhost:8080/");
public static void main(String[] args) throws Exception {
ResourceConfig config = new ResourceConfig()
.register(MenuResource.class)
.packages("com.example"); // or list resource classes explicitly
HttpServer server = JdkHttpServerFactory.createHttpServer(BASE_URI, config);
System.out.println("Server started at " + BASE_URI);
System.out.println("API available at " + BASE_URI + "api/menu");
System.out.println("Press Ctrl+C to stop.");
// Keep running until interrupted
Thread.currentThread().join();
}
}
ResourceConfig is a Jersey‑specific subclass of Application that allows programmatic registration of resources and features.
The packages() method scans the given package for JAX‑RS annotated classes, which is convenient when you have many resources.
Building and Running
Add the exec plugin to your pom.xml to run directly from Maven:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>com.example.Main</mainClass>
</configuration>
</plugin>
Then build and run:
mvn clean compile exec:java
Test the endpoints using curl:
# Get all menu items
curl http://localhost:8080/api/menu
# Get a specific item
curl http://localhost:8080/api/menu/1
# Create a new item
curl -X POST http://localhost:8080/api/menu \
-H "Content-Type: application/json" \
-d '{"name":"Pasta","price":9.99}'
# Update an item
curl -X PUT http://localhost:8080/api/menu/1 \
-H "Content-Type: application/json" \
-d '{"name":"Cheeseburger","price":10.99}'
# Delete an item
curl -X DELETE http://localhost:8080/api/menu/1
Exception Handling with ExceptionMapper
To handle exceptions globally and return meaningful HTTP responses, implement an ExceptionMapper.
For example, handling NotFoundException:
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
@Override
public Response toResponse(NotFoundException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Resource not found")
.build();
}
}
Register it in your ResourceConfig:
ResourceConfig config = new ResourceConfig()
.register(MenuResource.class)
.register(NotFoundExceptionMapper.class)
.packages("com.example");
Now, if any resource method throws NotFoundException, the mapper will produce a 404 response with the custom message.
Customising JSON with Jackson
Jersey’s Jackson integration allows you to use annotations to control serialization. For example, you can:
- Rename a field using
@JsonProperty. - Ignore fields with
@JsonIgnore. - Use
@JsonFormatfor date formatting.
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class MenuItem {
@JsonIgnore
private int internalId; // not serialized
@JsonProperty("id")
private int id;
@JsonProperty("itemName")
private String name;
// ... getters/setters
}
The Jackson provider is automatically enabled when you include jersey-media-json-jackson in your dependencies.
Deployment Options
The JDK HTTP server approach is great for microservices, embedded applications, and demos. However, for production, you might consider:
- Servlet containers – deploy your application as a WAR file to Tomcat, Jetty, or any Servlet 5+ container (Jakarta EE 9+). Use the
jersey-container-servletdependency. - Spring Boot – Spring Boot 3 uses Jakarta EE 9 and has built‑in support for Jersey (you can replace Spring MVC with Jersey).
- Helidon SE – a microservices framework that also uses Jakarta REST and can be run with the JDK HTTP server.
For a simple, self‑contained application, the JDK HTTP server is a lightweight and sufficient choice.
Jersey 2 vs Jersey 3 Summary
Summary
Jersey 3 implements the Jakarta REST 3 specification with the jakarta.ws.rs package namespace (replacing javax.ws.rs from Jersey 2).
To build a REST API:
- Add the Jersey 3 dependencies (core, JDK container, HK2, and Jackson).
- Create an
Applicationsubclass (orResourceConfig) to configure the app. - Annotate resource classes with
@Path,@GET/@POST/etc. - Start the server using the JDK HTTP container with
JdkHttpServerFactory.
The jersey-hk2 dependency provides dependency injection, and jersey-media-json-jackson handles JSON serialization automatically.
For production, consider deploying to a Servlet container or using a framework like Spring Boot, but the JDK HTTP server is perfect for quick, standalone services.
Happy coding!