Java: Record on Quarkus/ Newer Java

As I continue re-learning Java after my time away since Java 8, one of the most pleasant surprises has been the record keyword.

If you remember the pain of creating POJOs (Plain Old Java Objects) in the past—private fields, getters, setters, equals(), hashCode(), toString()—you know it was mostly noise. Lombok helped, but now, this capability is baked right into the language.

In this post, we’ll look at what Records are, why they make perfect Data Transfer Objects (DTOs), and how to use them in a Quarkus REST API.

What is a Record?

Introduced officially in Java 16, a Record is a special kind of class that acts as a transparent carrier for immutable data.

The Old Way (Java 8)

To create a simple object to hold a user’s data, we had to write this:

public class UserDTO {
    private final String name;
    private final String email;

    public UserDTO(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }

    // ... plus equals(), hashCode(), and toString()
}

The New Way (Java 16+)

With Records, the same can be achieved with much less boilerplate:

public record UserDTO(String name, String email) {}

The compiler automatically provides the constructor, getters (called accessors), and implementations for equals(), hashCode(), and toString().

That’s it. It is immutable by default, meaning once it’s created, the data cannot change. This makes it thread-safe and perfect for passing data around.

DTOs and Records: A Perfect Match

DTO (Data Transfer Object) is a design pattern. It’s an object used to transport data between processes or layers (e.g., from your API to your UI).

Why we use DTOs:

  • Decoupling: They separate the internal data representation from the external API.
  • Validation: They can enforce data constraints.
  • Clarity: They make it clear what data is being transferred.

Because DTOs are primarily about carrying data, Records are an excellent fit. They provide a concise way to define these data carriers without unnecessary boilerplate. So when you need a DTO, consider using a Record. Record in Java is created specifically for this purpose.

Using Records in a Quarkus REST API

Let’s see how to use Records in a Quarkus REST API.

Typical structure of the project:

src/main/java/com/example/
    ├── dto/
    │   └── UserDTO.java
    └── controllers/
        └── UserController.java
  1. Define the Record:
public record UserDTO(String name, String email) {}
  1. Create a REST Endpoint:
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import com.example.dto.UserDTO;

@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {
    @POST
    public Response createUser(UserDTO user) {
        // Process the user data (e.g., save to database)
        System.out.println("Creating user: " + user.name() + " with email: " + user.email());
        return Response.status(Response.Status.CREATED).entity(user).build();
    }

    @GET
    @Path("/{id}")
    public Response getUser(@PathParam("id") Long id) {
        // Simulate fetching user from database
        UserDTO user = new UserDTO("John Doe", "john@example.com");
        return Response.ok(user).build();
    }
}

Sample Request and Response Payloads

Creating a User (POST Request)

Request:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice Johnson",
    "email": "alice@example.com"
  }'

Response:

{
  "name": "Alice Johnson",
  "email": "alice@example.com"
}

Retrieving a User (GET Request)

Request:

curl -X GET http://localhost:8080/users/1

Response:

{
  "name": "John Doe",
  "email": "john@example.com"
}

How Records Help with JSON Serialization

One of the biggest advantages of using Records as DTOs is how seamlessly they work with JSON serialization/deserialization:

  1. Automatic Field Mapping: The record components automatically map to JSON fields
  2. Clean JSON Output: No extra boilerplate fields or methods are serialized
  3. Type Safety: The compiler ensures all required fields are present
  4. Immutability: Once created, the data cannot be accidentally modified

Compare this to traditional POJOs where you might accidentally serialize internal state or have inconsistent field naming.

Adding Validation with Records

Records work excellently with Bean Validation (JSR-303). Let’s enhance our UserDTO with validation.

Before adding validation, our record looks like this:

public record UserDTO(String name, String email) {}

Now, we improve it by adding validation annotations:

import jakarta.validation.constraints.*;

public record UserDTO(
    @NotBlank(message = "Name cannot be blank")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    String name,

    @NotBlank(message = "Email cannot be blank")
    @Email(message = "Email should be valid")
    String email
) {}

PS. It’s the same record definition, but with validation annotations. So, this solves the problem of validating incoming data in a clean way.

Update your controller to handle validation:

import jakarta.validation.Valid;

@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {

    @POST
    public Response createUser(@Valid UserDTO user) {
        // Validation happens automatically before this method is called
        System.out.println("Creating user: " + user.name() + " with email: " + user.email());
        return Response.status(Response.Status.CREATED).entity(user).build();
    }

    // ... rest of the methods
}

The @Valid annotation ensures that the incoming UserDTO is validated according to the constraints defined in the record. If validation fails, Quarkus will automatically return a 400 Bad Request response with details about the violations.

Validation in Action

Invalid Request:

curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "",
    "email": "invalid-email"
  }'

Error Response:

{
  "title": "Constraint Violation",
  "status": 400,
  "violations": [
    {
      "field": "name",
      "message": "Name cannot be blank"
    },
    {
      "field": "email",
      "message": "Email should be valid"
    }
  ]
}

Advanced Record Features

Custom Methods in Records

While Records are primarily for data, you can add custom methods:

public record UserDTO(
    @NotBlank String name,
    @NotBlank @Email String email
) {
    // Custom method to get display name
    public String displayName() {
        return name.toUpperCase();
    }

    // Validation in constructor
    public UserDTO {
        if (name != null && name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
    }
}

Nested Records for Complex Data

public record Address(String street, String city, String zipCode) {}

public record UserProfileDTO(
    @NotBlank String name,
    @NotBlank @Email String email,
    @Valid Address address
) {}

Why Records are Perfect for Modern Java Development

  1. Less Boilerplate: Focus on business logic, not getter/setter noise
  2. Immutability by Default: Thread-safe and predictable
  3. Pattern Matching Ready: Works great with newer Java features like pattern matching (Java 17+)
  4. Validation Friendly: Integrates seamlessly with Bean Validation
  5. JSON Serialization: Works out-of-the-box with Jackson and other JSON libraries
  6. Performance: Often more memory-efficient than traditional classes

Conclusion

Records represent a significant step forward in Java’s evolution. They eliminate much of the boilerplate code that made Java verbose while providing type safety and immutability.

For REST APIs in Quarkus, Records make excellent DTOs. They’re concise, safe, and integrate well with validation and serialization frameworks. If you’re working with Java 16+ (and you should be!), start using Records for your data transfer objects.

Next time you need to create a DTO, skip the traditional class and reach for a Record. Your future self will thank you for the cleaner, more maintainable code.

December 14, 2025 · 6 min

JS: Express.js

Express.js is a minimalist and flexible Node.js web application framework that provides a robust set of features for building web and mobile applications and APIs. If you’re looking to build fast, scalable, and efficient server-side applications with JavaScript, Express.js is your go-to choice. Its unopinionated nature gives you immense freedom,

Setting Up

Getting started with Express.js is straightforward. If you already have a Node.js project, or if you’re starting a new one, follow these simple steps:

  • Initializing a Node.js project (npm init).
  • Installing Express.js (npm install express).

Now, you’re ready to create your first Express server!

Core Concepts

Understanding these fundamental concepts is key to mastering Express.js:

Routing

Routing is the process of determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, PUT, DELETE, etc.).

In Express.js, you define routes using methods corresponding to HTTP verbs on the app object (an instance of Express).

HTTP Methods (Verbs): These map directly to common operations:

  • GET: Retrieve data (e.g., fetching a list of users).
  • POST: Submit data to be processed (e.g., creating a new user).
  • PUT: Update existing data (e.g., modifying a user’s details).
  • DELETE: Remove data (e.g., deleting a user).
const express = require("express");
const app = express();
const port = 3000;

// GET request to the root URL
app.get("/", (req, res) => {
  res.send("Hello from Express!");
});

// GET request to /users
app.get("/users", (req, res) => {
  res.json([
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ]);
});

// POST request to /users
app.post("/users", (req, res) => {
  // Logic to create a new user
  res.status(201).send("User created successfully!");
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});
// Route Parameters:
// Example URL: /users/123
app.get("/users/:id", (req, res) => {
  const { id } = req.params;
  res.send(`Fetching user with ID: ${id}`);
});

// Query Parameters:
// Example URL: /search?q=nodejs&category=backend
app.get("/search", (req, res) => {
  const { q: searchTerm, category } = req.query;
  res.send(`Searching for "${searchTerm}" in "${category}" category.`);
});

Middleware

Middleware functions are core to Express.js. They are functions that have access to the request object (req), the response object (res), and the next() function in the application’s request-response cycle. The next() function is crucial as it passes control to the next middleware function.

Think of middleware as a series of steps a request goes through before reaching its final route handler.

  • How Middleware Works: Requests flow sequentially through middleware functions. Each function can perform operations, modify req or res, end the cycle by sending a response, or pass control to the next middleware via next().

  • Common Use Cases:

    • Logging: Recording details about incoming requests.
    • Authentication/Authorization: Verifying user credentials and permissions.
    • Body Parsing: Express itself doesn’t parse request bodies by default. Middleware like express.json() and express.urlencoded() are essential for handling JSON and URL-encoded form data.
    • Serving Static Files: Handled by express.static().
    • Error Handling: Special middleware for catching and processing errors.

Example of Custom Middleware:

// A simple logger middleware
const logger = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // Pass control to the next handler
};

app.use(logger); // Use the middleware for all incoming requests

app.get("/", (req, res) => {
  res.send("Home Page with Logger!");
});

Handling Request (req) and Response (res)

  • req (Request Object):
    • Accessing request headers (req.header).
    • Accessing route and query parameters (req.params, req.query).
    • Accessing the request body (req.body).
  • res (Response Object):
    • Sending various types of responses: res.send(), res.json(), res.sendFile().
    • Setting HTTP status codes: res.status().
    • Redirecting requests: res.redirect().
    • Chaining methods (e.g., res.status(200).json(...)).

Typical Project Structure

While Express is unopinionated, a well-organized project structure is crucial for maintainability and scalability, especially as your application grows. Here’s a common pattern:

my-express-app/
├── node_modules/
├── server.js             # Entry point: Starts the server, sets up environment
├── app.js                # Express app configuration: Defines middleware, connects routes
├── routes/               # Defines API endpoints and links to controllers
│   ├── auth.routes.js
│   └── user.routes.js
├── controllers/          # Contains request handlers: Logic for specific routes, interacts with services
│   ├── auth.controller.js
│   └── user.controller.js
├── services/             # Business logic: Handles complex operations, orchestrates data flow
│   ├── auth.service.js
│   └── user.service.js
├── models/               # Data schema definitions (e.g., Mongoose schemas, Sequelize models)
│   ├── User.js
│   └── Product.js
├── middlewares/          # Custom middleware functions
│   ├── auth.middleware.js
│   └── error.middleware.js
├── config/               # Configuration files (database settings, environment variables)
│   └── db.config.js
├── package.json
├── package-lock.json
└── .env                  # Environment variables

Sample Project

( coming soon )

June 23, 2025 · 4 min