In today’s digital landscape, APIs are the backbone of modern applications, powering everything from mobile apps to complex microservices architectures. With that, increasing reliance comes an even greater need for robust security. Unauthorized access, data leaks, and misconfigured authentication can all lead to costly breaches. That’s why securing your API isn’t just an option—it’s a necessity.
In this tutorial, we’ll walk you through building a secure and scalable RESTful API using Quarkus, a modern Java framework designed for cloud-native development. We’ll connect it to a PostgreSQL database for data persistence, place it behind the Kong API Gateway for centralized traffic control and plugin-based enhancements, and secure it using OAuth2 for token-based authentication.
By the end of this guide, you’ll have a fully functional, secure API that:
- Connects to a PostgreSQL database using JPA and Hibernate
- Secures endpoints using OAuth2 access tokens (via Keycloak or any OAuth2 provider)
- Exposes the API through Kong, enabling centralized control and token validation
This tutorial is perfect for developers who want a complete, real-world setup for building and securing APIs in production environments. Whether you're deploying a microservices architecture or building a backend for a mobile app, the principles and stack used here will set you up for success.
Project Setup
Before we dive into building the API, let’s set up the development environment and scaffold our Quarkus project.
Prerequisites
Make sure you have the following tools installed:
- Java 17 or higher
- Apache Maven 3.8+
- Docker & Docker Compose
- curl or Postman (for testing APIs)
Initialize the Quarkus Project
We'll use the Quarkus Maven plugin to scaffold the project.
Run the following command in your terminal:
mvn io.quarkus:quarkus-maven-plugin:3.23.2:create \
-DprojectGroupId=com.djamware \
-DprojectArtifactId=secure-api \
-DclassName="com.djamware.resource.ProductResource" \
-Dpath="/products" \
-Dextensions="resteasy-reactive,jpa,hibernate-orm,quarkus-jdbc-postgresql,oidc,hibernate-orm-panache"
This command sets up a Quarkus project with:
- RESTEasy Reactive: For building reactive REST APIs
- JPA + Hibernate ORM: For database interaction
- PostgreSQL: As our database
- OIDC: To enable OAuth2 support
Navigate to the new project folder:
cd secure-api
Project Structure Overview
The generated project should have a structure like this:
We’ll be adding our entity, repository, and database configuration next.
Configuring PostgreSQL with Docker and Integrating it into Quarkus
To keep things clean and reproducible, we’ll run PostgreSQL using Docker and connect it to our Quarkus app using JDBC and Hibernate ORM.
Step 1: Create a docker-compose.yml
In the root of your project (secure-api/), create a file named docker-compose.yml with the following content:
version: '3.8'
services:
postgres:
image: postgres:15
container_name: quarkus_postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_DB: securedb
POSTGRES_USER: quarkus
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
✅ This sets up a PostgreSQL database named securedb with the user quarkus and password secret.
Start the container:
docker compose up -d
Step 2: Configure Quarkus to Connect to PostgreSQL
Open the src/main/resources/application.properties file and replace its contents with:
# Database Configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus
quarkus.datasource.password=secret
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/securedb
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
# Enable OIDC (OAuth2) for later steps
quarkus.oidc.auth-server-url=http://localhost:8080/realms/secure-api
quarkus.oidc.client-id=secure-api-client
quarkus.oidc.credentials.secret=secret
quarkus.oidc.tls.verification=none
quarkus.oidc.application-type=service
🚨 We’re using drop-and-create for development only. For production, switch to validate or update.
Step 3: Create a Sample Entity and Repository
Let’s build a simple Product entity.
Product.java
Create a file at src/main/java/com/djamware/model/Product.java:
package com.djamware.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(nullable = false)
public String name;
public String description;
public double price;
}
ProductRepository.java
Create src/main/java/com/djamware/repository/ProductRepository.java:
package com.djamware.repository;
import com.djamware.model.Product;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ProductRepository implements PanacheRepository<Product> {
}
Building the CRUD API with Quarkus RESTEasy Reactive
We'll now expose a RESTful API at /products to manage product data stored in PostgreSQL.
Folder Structure So Far
Your project should now look something like:
Step 1: Create the REST Resource
In src/main/java/com/djamware/resource/ProductResource.java, define the endpoints:
package com.djamware.resource;
import java.util.List;
import com.djamware.model.Product;
import com.djamware.repository.ProductRepository;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@Inject
ProductRepository repository;
@GET
public List<Product> getAll() {
return repository.listAll();
}
@GET
@Path("/{id}")
public Product getById(@PathParam("id") Long id) {
return repository.findById(id);
}
@POST
@Transactional
public Product create(Product product) {
repository.persist(product);
return product;
}
@PUT
@Path("/{id}")
@Transactional
public Product update(@PathParam("id") Long id, Product updated) {
Product product = repository.findById(id);
if (product == null) {
throw new NotFoundException();
}
product.name = updated.name;
product.description = updated.description;
product.price = updated.price;
return product;
}
@DELETE
@Path("/{id}")
@Transactional
public void delete(@PathParam("id") Long id) {
repository.deleteById(id);
}
}
✅ All methods are reactive but run in blocking mode since we’re using JPA/Hibernate with a JDBC datasource.
Test the Endpoints
Start the Quarkus app:
./mvnw quarkus:dev
Now test the endpoints using Postman or curl. Example:
curl -X POST http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"name":"Laptop","description":"Linux Dev Machine","price":999.99}'
You now have a fully working REST API!
Securing the API with OAuth2 and Keycloak
OAuth2 is the industry standard for securing APIs. Quarkus makes it simple to integrate using the quarkus-oidc extension. We’ll use Keycloak, an open-source identity and access management solution, as our OAuth2 provider.
Step 1: Run Keycloak with Docker
Add Keycloak to your existing docker-compose.yml:
keycloak:
image: quay.io/keycloak/keycloak:24.0
container_name: quarkus_keycloak
command: start-dev
ports:
- "8080:8080"
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
Update and start the services:
docker compose up -d
Access Keycloak: http://localhost:8080
Step 2: Configure Keycloak Realm and Client
1. Log into Keycloak Admin Console (admin/admin)
2. Create a new realm named secure-api by clicking the "Keycloak" dropdown
3. Create a new client (click the Client menu):
- Client ID: secure-api-client
- Client Protocol: openid-connect
- Access Type: confidential
- Root URL: http://localhost:8080
- Save, then set Valid Redirect URIs to * and copy the Client Secret (make sure the Client authentication is on)
4. Create a Role (e.g., user)
5. Create a User:
- Username: testuser
- Password: test123
- Assign the user role to this user
Step 3: Update Quarkus application.properties
Update your application.properties with the Keycloak client config:
# Enable OIDC (OAuth2) for later steps
quarkus.oidc.auth-server-url=http://localhost:8080/realms/secure-api
quarkus.oidc.client-id=secure-api-client
quarkus.oidc.credentials.secret=<YOUR_KEYCLOACK_CLIENT_SECRET>
quarkus.oidc.tls.verification=none
quarkus.oidc.application-type=service
# Require auth for all endpoints
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
Step 4: Secure Endpoints with Roles
Update ProductResource.java to restrict access:
import jakarta.annotation.security.RolesAllowed;
@Path("/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RolesAllowed("user")
public class ProductResource {
// ... same as before
}
Step 5: Obtain Access Token & Test
Before re-running the Quarkus app, change the port in application.properties:
quarkus.http.port=8081
Use curl or Postman to get a token:
curl -X POST http://localhost:8080/realms/secure-api/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=secure-api-client" \
-d "client_secret=<COPY_FROM_KEYCLOAK>" \
-d "username=testuser" \
-d "password=test123"
Use the access_token in an API call:
curl -H "Authorization: Bearer <ACCESS_TOKEN>" http://localhost:8081/products
Unauthenticated or invalid tokens will result in 401 Unauthorized.
Setting Up Kong API Gateway to Secure and Route Traffic to Quarkus
In this section, we’ll deploy Kong Gateway in front of our Quarkus API. Kong will act as a reverse proxy, handling:
- Routing
- Token validation (via plugin)
- Monitoring and throttling
- Future extensibility (rate limits, logging, etc.)
We’ll keep it simple to start — routing and securing our /products endpoint.
Step 1: Add Kong to Docker Compose
Extend your existing docker-compose.yml:
kong-db:
image: postgres:13
container_name: kong_postgres
environment:
POSTGRES_DB: kong
POSTGRES_USER: kong
POSTGRES_PASSWORD: kong
ports:
- "5433:5432"
kong-migrations:
image: kong/kong-gateway:3.6
command: kong migrations bootstrap
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-db
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong
depends_on:
- kong-db
kong:
image: kong/kong-gateway:3.6
container_name: kong
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-db
KONG_PG_PASSWORD: kong
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_ADMIN_LISTEN: "0.0.0.0:8001, 0.0.0.0:8444 ssl"
ports:
- "8000:8000" # Public proxy
- "8443:8443" # Public proxy SSL
- "8001:8001" # Admin API
- "8444:8444" # Admin API SSL
depends_on:
- kong-db
Then run:
docker compose up -d kong-db kong-migrations kong
Wait a few seconds for Kong to fully initialize.
Step 2: Verify Kong Admin API
Check that the Kong Admin API is accessible:
curl http://localhost:8001
You should get a JSON response with Kong’s version and configuration.
Step 3: Add Your Quarkus API as a Service
curl -i -X POST http://localhost:8001/services \
--data name=quarkus-api \
--data url=http://host.docker.internal:8081
host.docker.internal is used to reach Quarkus from inside the Kong container (macOS, Windows). On Linux, use 172.17.0.1 or the IP of your host.
Step 4: Add a Route for /products
curl -i -X POST http://localhost:8001/services/quarkus-api/routes \
--data 'paths[]=/products'
You can now access your API through Kong:
curl http://localhost:8000/products
At this point, the /products endpoint is publicly accessible through Kong.
Step 5: Secure with OIDC Plugin (Optional for Now)
Kong supports token validation via the OIDC plugin, which can be configured to validate Keycloak tokens. If you'd like, I can walk you through setting that up next, or we can move on to deploying everything with Docker Compose in production.
Securing Kong Routes with Keycloak via OIDC Plugin
We'll configure Kong to:
- Validate access tokens issued by Keycloak
- Forward authenticated requests to your Quarkus API
- Reject unauthorized requests
1. Ensure Prerequisites
- Kong is running with Admin API exposed at http://localhost:8001
- Keycloak is running, and a realm (e.g., secure-api) is set up
- A public client (or confidential with secret) is created in Keycloak
- Your Quarkus API works at http://localhost:8081/products
2. Test Your Keycloak Setup
From Keycloak:
1. Create a user and assign them a password.
2. Get a token using password grant:
curl -X POST http://localhost:8080/realms/secure-api/protocol/openid-connect/token \
-d "client_id=my-client" \
-d "grant_type=password" \
-d "username=myuser" \
-d "password=mypassword" \
-d "client_secret=YOUR_SECRET" # Only for confidential clients
You should receive a JSON with an access_token.
3. Enable the OIDC Plugin on Kong’s /products route
First, get the route ID:
curl http://localhost:8001/routes
Look for the route pointing to /products and note its "id" (e.g., abc123).
Now, apply the OIDC plugin to that route:
curl -i -X POST http://localhost:8001/routes/ROUTE_ID/plugins \
--data "name=openid-connect" \
--data "config.issuer=http://host.docker.internal:8080/realms/secure-api" \
--data "config.client_id=secure-api-client" \
--data "config.client_secret=YOUR_SECRET" \
--data "config.auth_methods=password" \
--data "config.scopes=openid"
Use host.docker.internal so Kong (in Docker) can reach Keycloak (on host).
Use config.discovery if you want to provide full .well-known/openid-configuration.
4. Test Secured Access
Try accessing /products without a token:
curl http://localhost:8000/products
You should get: HTTP 401 Unauthorized
Now try with a token:
curl http://localhost:8000/products \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
You should get the JSON product list from Quarkus.
Optional Enhancements
- Enforce role-based access: Add config.roles_claim and config.required_scope
- Add token introspection instead of JWT validation
- Secure all routes via a global plugin
Final Thoughts
Building a secure API requires careful consideration of authentication, authorization, and network architecture. In this tutorial, we combined several powerful tools—Quarkus for building reactive and efficient Java APIs, PostgreSQL as a robust relational database, Keycloak for identity and access management, and Kong as a scalable API gateway.
By the end of this setup, you’ve achieved:
- A CRUD API implemented with Quarkus and PostgreSQL
- Identity and token management using Keycloak and OAuth2
- Centralized routing and security enforcement using Kong’s OIDC plugin
This architecture not only secures your API endpoints but also makes it easier to manage authentication at scale, enforce policies consistently, and prepare your application for production traffic.
In the next section, we’ll go even further by implementing role-based access control (RBAC) and rate limiting—key components for building resilient and enterprise-ready APIs.
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about Java and Spring Framework, you can take the following cheap course:
- Java basics, Java in Use //Complete course for beginners
- Java Programming: Master Basic Java Concepts
- Master Java Web Services and REST API with Spring Boot
- JDBC Servlets and JSP - Java Web Development Fundamentals
- The Complete Java Web Development Course
- Spring MVC For Beginners: Build Java Web App in 25 Steps
- Practical RESTful Web Services with Java EE 8 (JAX-RS 2.1)
Thanks!