We had seen how to use JAX-RS 3 with Jetty in a previous blog post. Here we will see how to run a JAX-RS 3 API server in Google App Engine Standard. We are using standard edition because it does auto scaling, is cost effective and supports Java out of the box. We will be using JDK 21 and corresponding dependencies.

The project structure is as follows.

APIZenServer
├── README.md
├── build
├── pom.xml
└── src
    └── main
        ├── java
        │   └── net
        │       └── apizen
        │           └── apiserver
        │               ├── APIZenApplication.java
        │               ├── model
        │               │   └── Health.java
        │               └── resource
        │                   └── HealthResource.java
        └── webapp
            └── WEB-INF
                ├── appengine-web.xml
                └── web.xml

We will see each file below.

// APIZenApplication.java

package net.apizen.apiserver;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import net.apizen.apiserver.resource.HealthResource;

import java.util.HashSet;
import java.util.Set;

@ApplicationPath("/api")
public class APIZenApplication extends Application {
    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        classes.add(HealthResource.class);
        return classes;
    }
}
// Health.java

package net.apizen.apiserver.model;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Health {
    private String status;
    private String server = "api-zen";
    private String health;

    private final ObjectMapper om = new ObjectMapper();

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getServer() {
        return server;
    }

    public void setServer(String server) {
        this.server = server;
    }

    public String getHealth() {
        return health;
    }

    public void setHealth(String health) {
        this.health = health;
    }

    public String toJSON() throws JsonProcessingException {
        return om.writeValueAsString(this);
    }
}
// HealthResource.java

package net.apizen.apiserver.resource;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import net.apizen.apiserver.model.Health;

@Path("/")
public class HealthResource {
    @GET
    @Path("/health")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getHealth() {
        Health health = new Health();
        health.setStatus("true");
        health.setHealth("ok");
        try {
            return Response.ok(health.toJSON()).build();
        } catch (JsonProcessingException e) {
            return Response.serverError().build();
        }
    }
}
<!-- appengine-web.xml -->

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
  <runtime>java21</runtime>
  <app-engine-apis>true</app-engine-apis>
  <instance-class>F1</instance-class>
  <automatic-scaling>
    <target-cpu-utilization>0.85</target-cpu-utilization>
    <max-instances>2</max-instances>
  </automatic-scaling>
</appengine-web-app>
<!-- web.xml -->

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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"
         version="6.0">

  <display-name>API Zen API Server</display-name>

  <servlet>
    <servlet-name>JerseyServlet</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>jakarta.ws.rs.Application</param-name>
      <param-value>net.apizen.apiserver.APIZenApplication</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>JerseyServlet</servlet-name>
    <url-pattern>/api/*</url-pattern>
  </servlet-mapping>

</web-app>
<!-- pom.xml -->

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <!-- Application must be packed as a .war to deploy on App Engine Standard -->
  <packaging>war</packaging>
  <groupId>net.apizen</groupId>
  <artifactId>api-server</artifactId>
  <version>1.0</version>

  <properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <jersey.version>3.1.10</jersey.version>
    <jetty.version>11.0.25</jetty.version>
  </properties>

  <dependencies>
    <!-- Jersey -->
    <dependency>
      <groupId>org.glassfish.jersey.containers</groupId>
      <artifactId>jersey-container-jetty-servlet</artifactId>
      <version>${jersey.version}</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.inject</groupId>
      <artifactId>jersey-hk2</artifactId>
      <version>${jersey.version}</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.jersey.media</groupId>
      <artifactId>jersey-media-json-binding</artifactId>
      <version>${jersey.version}</version>
    </dependency>

    <!-- Jetty -->
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-servlet</artifactId>
      <version>${jetty.version}</version>
    </dependency>

    <!-- JSON -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.18.3</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate.orm</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>6.6.12.Final</version>
    </dependency>

    <!-- HikariCP -->
    <dependency>
      <groupId>com.zaxxer</groupId>
      <artifactId>HikariCP</artifactId>
      <version>6.3.0</version>
    </dependency>

    <!-- PostgreSQL Driver -->
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>42.7.5</version>
    </dependency>

    <!-- App Engine SDK dependency : only required if you need to explicitly use App Engine API -->
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-api-1.0-sdk</artifactId>
      <version>2.0.23</version>
    </dependency>
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.1.0</version>
      <type>jar</type>
      <scope>provided</scope>
    </dependency>

    <!-- Test Dependencies -->
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-testing</artifactId>
      <version>2.0.23</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-api-stubs</artifactId>
      <version>2.0.23</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-tools-sdk</artifactId>
      <version>2.0.23</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>com.google.truth</groupId>
      <artifactId>truth</artifactId>
      <version>1.1.5</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>4.11.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <!-- For hot reload of the web application -->
    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</outputDirectory>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.4.0</version>
      </plugin>
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>appengine-maven-plugin</artifactId>
        <version>2.8.0</version>
        <configuration>
          <projectId>${app.deploy.projectId}</projectId>
          <version>GCLOUD_CONFIG</version>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
# build

#!/bin/bash

function help() {
  echo "Usage:"
  echo "./build run"
  echo "./build deploy-to-stg"
  echo "./build deploy-to-prod"
}

function deploy_to_stg() {
  echo "Deploying to staging"
  mvn clean package appengine:deploy -Dapp.deploy.projectId=stg-server-id-here
}

function deploy_to_prod() {
  echo "Deploying to production"
  mvn clean package appengine:deploy -Dapp.deploy.projectId=prod-server-id-here
}

function run() {
  echo "Starting the dev server"
  mvn package appengine:run
}

opt=$1
if [ -z "$opt" ]; then
  help
else
  case $opt in
    "run")
      run
      ;;
    "deploy-to-stg")
      deploy_to_stg
      ;;
    "deploy-to-prod")
      deploy_to_prod
      ;;
    *)
      echo "Unknown argument"
      exit 1
      ;;
  esac
fi

exit 0

Replace the project ids in the above code.

Build commands are given below.

# Authenticate with Google Cloud
gcloud auth login

# Start the dev server
./build run

# Deploy to staging
./build deploy-to-stg

# Deploy to production
./build deploy-to-prod

Google App Engine project and API must be enabled in Google Cloud Console in order to deploy. This will give us a JAX-RS API server in Google Cloud. The App Engine parameters can be configured in the appengine-web.xml file.

This will give us one endpoint for health resource at /api/health. The server runs in local at http://localhost:8080.