How to Build a Minimal Enterprise Java REST API with JAX‑RS, CDI and OpenAPI
This tutorial walks through creating a lightweight RESTful web API for managing people using Java EE standards such as JAX‑RS, CDI, JPA, Bean Validation, OpenAPI, and the Eclipse MicroProfile stack, showing the full code, Maven setup, and integration testing.
In this tutorial we build a simple RESTful web API that demonstrates how to use Java EE (now Jakarta EE) and the Spring platform to create an enterprise‑grade Java application.
We start by defining the domain model PersonEntity with JPA annotations:
@Entity
@Table(name = "people")
public class PersonEntity {
@Id
@Column(length = 256)
private String email;
@Column(nullable = false, length = 256, name = "first_name")
private String firstName;
@Column(nullable = false, length = 256, name = "last_name")
private String lastName;
@Version
private Long version;
}The repository provides basic CRUD operations:
@ApplicationScoped
public class PeopleJpaRepository implements PeopleRepository {
@Inject @PeopleDb private EntityManager em;
@Transactional(readOnly = true)
public Optional<PersonEntity> findByEmail(String email) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<PersonEntity> query = cb.createQuery(PersonEntity.class);
Root<PersonEntity> root = query.from(PersonEntity.class);
query.where(cb.equal(root.get(PersonEntity_.email), email));
try {
PersonEntity entity = em.createQuery(query).getSingleResult();
return Optional.of(entity);
} catch (NoResultException ex) {
return Optional.empty();
}
}
@Transactional
public PersonEntity saveOrUpdate(String email, String firstName, String lastName) {
PersonEntity entity = new PersonEntity(email, firstName, lastName);
em.persist(entity);
return entity;
}
@Transactional(readOnly = true)
public Collection<PersonEntity> findAll() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<PersonEntity> query = cb.createQuery(PersonEntity.class);
query.from(PersonEntity.class);
return em.createQuery(query).getResultList();
}
@Transactional
public Optional<PersonEntity> deleteByEmail(String email) {
return findByEmail(email).map(entity -> { em.remove(entity); return entity; });
}
}We expose the service layer through PeopleServiceImpl which delegates to the repository and converts entities to DTOs:
@ApplicationScoped
public class PeopleServiceImpl implements PeopleService {
@Inject private PeopleRepository repository;
@Override
public Optional<Person> findByEmail(String email) {
return repository.findByEmail(email).map(this::toPerson);
}
@Override
public Person add(Person person) {
return toPerson(repository.saveOrUpdate(person.getEmail(), person.getFirstName(), person.getLastName()));
}
@Override
public Collection<Person> getAll() {
return repository.findAll().stream().map(this::toPerson).collect(Collectors.toList());
}
@Override
public Optional<Person> remove(String email) {
return repository.deleteByEmail(email).map(this::toPerson);
}
private Person toPerson(PersonEntity entity) {
return new Person(entity.getEmail(), entity.getFirstName(), entity.getLastName());
}
}The JAX‑RS application and resource classes define the REST endpoints and OpenAPI documentation:
@ApplicationPath("api")
@OpenAPIDefinition(info = @Info(title = "People Management Web APIs", version = "1.0.0", license = @License(name = "Apache License", url = "https://www.apache.org/licenses/LICENSE-2.0")))
public class PeopleApplication extends Application {}
@ApplicationScoped
@Path("/people")
@Tag(name = "people")
public class PeopleResource {
@Inject private PeopleService service;
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(description = "List all people", responses = {@ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Person.class))), responseCode = "200" )})
public Collection<Person> getPeople() {
return service.getAll();
}
@GET
@Path("/{email}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(description = "Find person by e‑mail", responses = {
@ApiResponse(content = @Content(schema = @Schema(implementation = Person.class)), responseCode = "200"),
@ApiResponse(responseCode = "404", description = "Person with such e‑mail doesn't exist") })
public Person findPerson(@Parameter(description = "E‑Mail address to lookup for", required = true) @PathParam("email") String email) {
return service.findByEmail(email).orElseThrow(() -> new NotFoundException("Person with such e‑mail doesn't exists"));
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(description = "Create new person", requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = Person.class))), responses = {
@ApiResponse(content = @Content(schema = @Schema(implementation = Person.class)), responseCode = "201", headers = @Header(name = "Location")),
@ApiResponse(responseCode = "409", description = "Person with such e‑mail already exists") })
public Response addPerson(@Context UriInfo uriInfo, @Valid Person payload) {
Person person = service.add(payload);
return Response.created(uriInfo.getRequestUriBuilder().path(person.getEmail()).build()).entity(person).build();
}
@DELETE
@Path("/{email}")
@Operation(description = "Delete existing person", responses = {
@ApiResponse(responseCode = "204", description = "Person has been deleted"),
@ApiResponse(responseCode = "404", description = "Person with such e‑mail doesn't exists") })
public Response deletePerson(@Parameter(description = "E‑Mail address to lookup for", required = true) @PathParam("email") String email) {
return service.remove(email).map(r -> Response.noContent().build())
.orElseThrow(() -> new NotFoundException("Person with such e‑mail doesn't exists"));
}
}Transaction management is handled declaratively with the @Transactional annotation provided by Apache DeltaSpike, and the CDI producer supplies the EntityManager:
@ApplicationScoped
public class PersistenceConfig {
@PersistenceUnit(unitName = "peopledb") private EntityManagerFactory entityManagerFactory;
@Produces @PeopleDb @TransactionScoped
public EntityManager create() { return entityManagerFactory.createEntityManager(); }
public void dispose(@Disposes @PeopleDb EntityManager em) { if (em.isOpen()) em.close(); }
}The Maven pom.xml pulls in the required dependencies (DeltaSpike, Hammock MicroProfile, OpenJPA, Flyway, Swagger, etc.) and defines the versions:
<properties>
<deltaspike.version>1.8.1</deltaspike.version>
<hammock.version>2.1</hammock.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.deltaspike.modules</groupId>
<artifactId>deltaspike-jpa-module-api</artifactId>
<version>${deltaspike.version}</version>
</dependency>
... (other dependencies omitted for brevity) ...
</dependencies>To run the application use Maven:
mvn clean package
java -jar target/eclipse-microprofile-hammock-0.0.1-SNAPSHOT-capsule.jarSample curl commands verify the API, including a successful POST that returns HTTP 201 with a Location header, and a validation failure that returns HTTP 400.
Integration testing is performed with Arquillian, JUnit and REST‑Assured. The test deploys the application archive and asserts that adding a new person returns HTTP 201 and the expected JSON fields:
@RunWith(Arquillian.class)
@EnableRandomWebServerPort
public class PeopleApiTest {
@ArquillianResource private URI uri;
@Deployment
public static JavaArchive createArchive() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(PeopleResource.class, PeopleApplication.class)
.addClasses(PeopleServiceImpl.class, PeopleJpaRepository.class, PersistenceConfig.class)
.addPackages(true, "org.apache.deltaspike");
}
@Test
public void shouldAddNewPerson() throws Exception {
Person person = new Person("[email protected]", "John", "Smith");
given()
.contentType(ContentType.JSON)
.body(person)
.post(uri + "/api/people")
.then()
.statusCode(201)
.body("email", equalTo("[email protected]"))
.body("firstName", equalTo("John"))
.body("lastName", equalTo("Smith"));
}
}The article concludes that modern Java EE development is enjoyable, especially with Spring‑inspired APIs, and points to the full source code on GitHub.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
