Building a Java Code Formatting API with Spring Boot & google-java-format | Web Formatter Blog

Building a Java Code Formatting API with Spring Boot & google-java-format
Create a robust REST API to format Java code using Spring Boot and Google's official formatter.
Introduction
Consistent code formatting is essential for maintainability in Java projects. While individual developers can use IDE plugins or command-line tools to format their code, providing a centralized formatting service offers significant advantages for teams and organizations.
In this tutorial, we'll build a REST API using Spring Boot that leverages Google's java-format library to provide on-demand Java code formatting as a service. This API can be integrated into CI/CD pipelines, IDEs, or custom tools to ensure consistent formatting across your entire organization.
Prerequisites
To follow along with this tutorial, you'll need:
- Java 17 or higher
- Maven or Gradle build tool
- Your favorite Java IDE (IntelliJ IDEA, Eclipse, VS Code)
- Basic knowledge of Spring Boot and REST APIs
- Postman or cURL for testing (optional)
Project Setup
Adding Dependencies
Start by creating a new Spring Boot project using Spring Initializr (https://start.spring.io/) with the following dependencies:
- Spring Web
- Spring Boot DevTools (optional, for development convenience)
- Lombok (optional, for reducing boilerplate code)
After generating and downloading the project, add the google-java-format dependency to your build file:
Project Structure
Organize your project with a clean structure:
src/main/java/com/example/codeformatter/
├── CodeFormatterApplication.java # Spring Boot main class
├── controller/
│ └── FormatterController.java # REST controller
├── service/
│ └── JavaFormatterService.java # Formatting service
├── model/
│ ├── FormatRequest.java # Request model
│ └── FormatResponse.java # Response model
└── exception/
├── FormatterException.java # Custom exceptions
└── GlobalExceptionHandler.java # Exception handling
Creating the Formatter Service
Integrating google-java-format
First, let's create the service that will handle the actual code formatting using the google-java-format library:
package com.example.codeformatter.service;
import com.google.googlejavaformat.java.Formatter;
import com.google.googlejavaformat.java.FormatterException;
import com.google.googlejavaformat.java.JavaFormatterOptions;
import org.springframework.stereotype.Service;
@Service
public class JavaFormatterService {
private final Formatter formatter;
public JavaFormatterService() {
// Use Google style by default
JavaFormatterOptions options = JavaFormatterOptions.builder()
.style(JavaFormatterOptions.Style.GOOGLE)
.build();
this.formatter = new Formatter(options);
}
public String formatCode(String unformattedCode) throws FormatterException {
if (unformattedCode == null || unformattedCode.trim().isEmpty()) {
return unformattedCode;
}
return formatter.formatSource(unformattedCode);
}
}
This service creates a formatter instance with Google's default formatting style and provides a method to format Java code.
Formatter Options
Google Java Format offers several configuration options. If you want to provide different formatting styles, you can extend the service:
public enum FormattingStyle {
GOOGLE,
AOSP
}
public String formatCode(String unformattedCode, FormattingStyle style) throws FormatterException {
JavaFormatterOptions options = JavaFormatterOptions.builder()
.style(style == FormattingStyle.GOOGLE ?
JavaFormatterOptions.Style.GOOGLE :
JavaFormatterOptions.Style.AOSP)
.build();
Formatter customFormatter = new Formatter(options);
return customFormatter.formatSource(unformattedCode);
}
The two built-in styles are:
- GOOGLE: The standard Google Java Style
- AOSP: Android Open Source Project style
Building the REST Controller
API Endpoint Design
For our REST API, we'll design simple endpoints that accept Java code as input and return formatted code:
-
POST /api/format
- Format Java code with default options -
POST /api/format/{"{style}"}
- Format Java code with specified style
Request/Response Models
Create model classes for the API requests and responses:
package com.example.codeformatter.model;
import lombok.Data;
@Data
public class FormatRequest {
private String code;
}
@Data
public class FormatResponse {
private String formattedCode;
private boolean success;
private String message;
public static FormatResponse success(String formattedCode) {
FormatResponse response = new FormatResponse();
response.setFormattedCode(formattedCode);
response.setSuccess(true);
return response;
}
public static FormatResponse error(String message) {
FormatResponse response = new FormatResponse();
response.setSuccess(false);
response.setMessage(message);
return response;
}
}
Controller Implementation
Now, let's implement the REST controller:
package com.example.codeformatter.controller;
import com.example.codeformatter.model.FormatRequest;
import com.example.codeformatter.model.FormatResponse;
import com.example.codeformatter.service.JavaFormatterService;
import com.google.googlejavaformat.java.FormatterException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class FormatterController {
private final JavaFormatterService formatterService;
@Autowired
public FormatterController(JavaFormatterService formatterService) {
this.formatterService = formatterService;
}
@PostMapping("/format")
public ResponseEntity formatCode(@RequestBody FormatRequest request) {
try {
String formattedCode = formatterService.formatCode(request.getCode());
return ResponseEntity.ok(FormatResponse.success(formattedCode));
} catch (FormatterException e) {
return ResponseEntity.badRequest().body(FormatResponse.error("Invalid Java code: " + e.getMessage()));
}
}
@PostMapping("/format/{style}")
public ResponseEntity formatCodeWithStyle(
@RequestBody FormatRequest request,
@PathVariable("style") String styleName) {
try {
JavaFormatterService.FormattingStyle style = JavaFormatterService.FormattingStyle.valueOf(styleName.toUpperCase());
String formattedCode = formatterService.formatCode(request.getCode(), style);
return ResponseEntity.ok(FormatResponse.success(formattedCode));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(FormatResponse.error("Invalid style: " + styleName));
} catch (FormatterException e) {
return ResponseEntity.badRequest().body(FormatResponse.error("Invalid Java code: " + e.getMessage()));
}
}
}
This controller provides two endpoints as planned and handles basic error cases.
Error Handling
Custom Exceptions
To improve the error handling, let's create custom exceptions:
package com.example.codeformatter.exception;
public class FormatterException extends RuntimeException {
public FormatterException(String message) {
super(message);
}
public FormatterException(String message, Throwable cause) {
super(message, cause);
}
}
public class InvalidCodeException extends FormatterException {
public InvalidCodeException(String message) {
super("Invalid Java code: " + message);
}
}
public class InvalidStyleException extends FormatterException {
public InvalidStyleException(String style) {
super("Invalid formatting style: " + style);
}
}
Global Exception Handler
Implement a global exception handler for consistent error responses:
package com.example.codeformatter.exception;
import com.example.codeformatter.model.FormatResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(InvalidCodeException.class)
public ResponseEntity handleInvalidCodeException(InvalidCodeException ex) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(FormatResponse.error(ex.getMessage()));
}
@ExceptionHandler(InvalidStyleException.class)
public ResponseEntity handleInvalidStyleException(InvalidStyleException ex) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(FormatResponse.error(ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity handleGenericException(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(FormatResponse.error("An unexpected error occurred: " + ex.getMessage()));
}
}
Now update the service and controller to use these custom exceptions.
Testing the API
Unit Tests
Let's write unit tests for the formatter service:
package com.example.codeformatter.service;
import com.example.codeformatter.exception.InvalidCodeException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class JavaFormatterServiceTest {
private final JavaFormatterService formatterService = new JavaFormatterService();
@Test
void shouldFormatValidJavaCode() {
String unformattedCode = "public class Test {public static void main(String[] args) {System.out.println(\"Hello\");}}";
String expectedCode = "public class Test {\\n public static void main(String[] args) {\\n System.out.println(\"Hello\");\\n }\\n}\\n";
String formattedCode = formatterService.formatCode(unformattedCode);
assertEquals(expectedCode.replace("\\n", System.lineSeparator()), formattedCode);
}
@Test
void shouldHandleEmptyInput() {
String emptyCode = "";
String result = formatterService.formatCode(emptyCode);
assertEquals(emptyCode, result);
}
@Test
void shouldThrowExceptionForInvalidJavaCode() {
String invalidCode = "public class Test {";
assertThrows(InvalidCodeException.class, () -> formatterService.formatCode(invalidCode));
}
}
Integration Tests
Write integration tests to verify the API endpoints:
package com.example.codeformatter.controller;
import com.example.codeformatter.model.FormatRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class FormatterControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldFormatValidCode() throws Exception {
FormatRequest request = new FormatRequest();
request.setCode("public class Test { void method() { int x = 10; } }");
mockMvc.perform(post("/api/format")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.formattedCode").isNotEmpty());
}
@Test
void shouldReturnErrorForInvalidCode() throws Exception {
FormatRequest request = new FormatRequest();
request.setCode("public class Test {");
mockMvc.perform(post("/api/format")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").isNotEmpty());
}
@Test
void shouldHandleInvalidStyle() throws Exception {
FormatRequest request = new FormatRequest();
request.setCode("public class Test { }");
mockMvc.perform(post("/api/format/invalidstyle")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("Invalid style")));
}
}
Postman Examples
Here's a quick guide to testing your API with Postman:
-
Create a new POST request to{" "}
http://localhost:8080/api/format
-
Set the Content-Type header to
application/json
- Add a request body:
{
"code": "public class Example { public static void main(String[] args) { System.out.println(\\\"Hello World\\\"); } }"
}
The response should look like:
{
"formattedCode": "public class Example {\\n public static void main(String[] args) {\\n System.out.println(\\\"Hello World\\\");\\n }\\n}\\n",
"success": true,
"message": null
}
Deployment Options
Containerizing with Docker
Create a Dockerfile
in your project root:
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Build and run the Docker container:
# Build the JAR
./mvnw clean package
# Build the Docker image
docker build -t java-formatter-api .
# Run the container
docker run -p 8080:8080 java-formatter-api
Cloud Deployment
This API can be deployed on various cloud platforms:
- AWS Elastic Beanstalk: Upload the JAR file or use Docker deployment
- Google Cloud Run: Perfect for containerized applications that handle HTTP requests
- Heroku: Simple deployment with the Java buildpack
- Azure App Service: Supports both JAR and Docker deployments
For most cloud platforms, you'll just need to:
- Create an account and set up billing
- Install the cloud provider's CLI tools
- Build your application
- Deploy using the platform-specific commands
Client Usage Examples
cURL Requests
A simple cURL command to format Java code:
curl -X POST \\
http://localhost:8080/api/format \\
-H 'Content-Type: application/json' \\
-d '{
"code": "public class Test { void method() {int x=10;} }"
}'
JavaScript/Fetch
For web applications, you can use the Fetch API:
async function formatJavaCode(code) {
try {
const response = await fetch('http://localhost:8080/api/format', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
});
const data = await response.json();
if (data.success) {
return data.formattedCode;
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error formatting code:', error);
throw error;
}
}
// Usage
formatJavaCode('public class Test { void method() {int x=10;} }')
.then(formattedCode => console.log(formattedCode))
.catch(error => console.error(error));
Java Client
Create a simple Java client using Spring's RestTemplate:
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class FormatterClient {
private final String apiUrl;
private final RestTemplate restTemplate;
public FormatterClient(String apiUrl) {
this.apiUrl = apiUrl;
this.restTemplate = new RestTemplate();
}
public String formatCode(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
FormatRequest request = new FormatRequest();
request.setCode(code);
HttpEntity entity = new HttpEntity<>(request, headers);
ResponseEntity response =
restTemplate.postForEntity(apiUrl + "/api/format", entity, FormatResponse.class);
FormatResponse formatResponse = response.getBody();
if (formatResponse != null && formatResponse.isSuccess()) {
return formatResponse.getFormattedCode();
} else {
throw new RuntimeException("Formatting failed: " +
(formatResponse != null ? formatResponse.getMessage() : "Unknown error"));
}
}
}
Securing Your API
Rate Limiting
To prevent abuse, implement rate limiting using Spring Boot's bucket4j integration:
com.giffing.bucket4j.spring.boot.starter
bucket4j-spring-boot-starter
0.7.0
Configure rate limiting in application.properties
:
bucket4j.enabled=true
bucket4j.filters[0].cache-name=rateLimit
bucket4j.filters[0].filter-method=servlet
bucket4j.filters[0].http-response-body={ "success": false, "message": "Rate limit exceeded" }
bucket4j.filters[0].url=/api.*
bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=20
bucket4j.filters[0].rate-limits[0].bandwidths[0].time=1
bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=minutes
Authentication Options
For secured deployments, add API key or JWT-based authentication:
org.springframework.boot
spring-boot-starter-security
Create a simple API key filter:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${"api.key"}")
private String apiKey;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.addFilterBefore(new ApiKeyAuthFilter(apiKey), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll();
}
}
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final String apiKey;
public ApiKeyAuthFilter(String apiKey) {
this.apiKey = apiKey;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestApiKey = request.getHeader("X-API-KEY");
if (apiKey.equals(requestApiKey)) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: Invalid API key");
}
}
}
Potential Enhancements
Additional Formatters
Extend your API to support other Java formatters:
- eclipse-formatter: Support Eclipse code style
- spring-javaformat: Spring's custom Java formatter
- prettier-java: Prettier plugin for Java
Create a common interface:
public interface CodeFormatter {
String format(String code) throws Exception;
}
@Service
public class FormatterFactory {
private final Map formatters = new HashMap<>();
@Autowired
public FormatterFactory(GoogleFormatter googleFormatter,
EclipseFormatter eclipseFormatter) {
formatters.put("google", googleFormatter);
formatters.put("eclipse", eclipseFormatter);
}
public CodeFormatter getFormatter(String formatterName) {
return formatters.getOrDefault(
formatterName.toLowerCase(),
formatters.get("google") // Default formatter
);
}
}
Customizable Format Options
Allow users to pass customizable formatting options:
public class FormatRequest {
private String code;
private Map options;
}
// In the service
public String formatWithOptions(String code, Map options) {
JavaFormatterOptions.Builder builder = JavaFormatterOptions.builder();
// Apply options
if (options.containsKey("style")) {
String style = (String) options.get("style");
if ("aosp".equalsIgnoreCase(style)) {
builder.style(JavaFormatterOptions.Style.AOSP);
} else {
builder.style(JavaFormatterOptions.Style.GOOGLE);
}
}
if (options.containsKey("indent")) {
int indent = ((Number) options.get("indent")).intValue();
// Note: google-java-format doesn't support custom indent size,
// this is just an example for formatters that do
}
Formatter customFormatter = new Formatter(builder.build());
return customFormatter.formatSource(code);
}
Conclusion
In this tutorial, we've built a robust REST API for Java code formatting using Spring Boot and Google's java-format library. This service can be a valuable addition to your development ecosystem, ensuring consistent code formatting across your organization.
You can extend this API in many ways, such as:
- Adding support for different formatters and style guides
- Implementing user-specific configurations
- Expanding to support other languages
- Integrating with code repositories to automatically format submitted code
- Building plugins for popular IDEs to use your custom formatter service
By centralizing your formatting service, you can ensure that everyone in your organization follows the same coding standards, leading to cleaner, more maintainable codebases.