How To Introduce a New API Quickly Using Spring Boot and Gradle

Written by johnjvester | Published 2025/03/24
Tech Story Tags: spring-boot | java | heroku | chatgpt | startup | cloud | gradle | hackernoon-top-story

TLDRTime to market can make or break any idea or solution. Check out how quickly a RESTful API can be created by leveraging ChatGPT, Spring Boot, Gradle, and Heroku.via the TL;DR App

For the last five years, Iā€™ve had the quote ā€œeverything begins with an ideaā€ on the wall of my office.

My wife found this product onĀ EtsyĀ shortly after I started developing an API collection for a fitness application. I love this statement because it captures the passion that consumes me during the creation stages of a new project. This is still my favorite aspect of being an engineer, even three decades into my career.

What Iā€™ve learned during this time is that an idea only matters if someone has the opportunity to experience it.Ā If an idea takes too long to become a reality, you end up with a missed opportunity as someone else beats you to the punch.Ā This is why startups are always racing to get their ideas to market as quickly as possible.

Letā€™s walk through how we can make an idea a reality ā€¦ quickly.

Assumptions

For this article, weā€™ll keep things simple. Weā€™ll use Java 17 and Spring Boot 3 to create a RESTful API. In this example, weā€™ll use Gradle for our build automation.

While the service idea we plan to take to market would normally use a persistence layer, weā€™ll set that aside for this example and statically define our data within a repository class.

We wonā€™t worry about adding any security for this example, simply allowing anonymous access for this proof of concept.

The Motivational Quotes API

Letā€™s assume our idea is a motivational quotes API. To make sure we are racing as fast as possible, I asked ChatGPT to create an OpenAPI spec for me.

Within seconds, ChatGPT provided the response:

Hereā€™s the OpenAPI specification, in YAML, that ChatGPT generated:

openapi: 3.0.0
info:
  title: Motivational Quotes API
  description: An API that provides motivational quotes.
  version: 1.0.0
servers:
  - url: https://api.example.com
    description: Production server
paths:
  /quotes:
    get:
      summary: Get all motivational quotes
      operationId: getAllQuotes
      responses:
        '200':
          description: A list of motivational quotes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Quote'
  /quotes/random:
    get:
      summary: Get a random motivational quote
      operationId: getRandomQuote
      responses:
        '200':
          description: A random motivational quote
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
  /quotes/{id}:
    get:
      summary: Get a motivational quote by ID
      operationId: getQuoteById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: A motivational quote
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
        '404':
          description: Quote not found
components:
  schemas:
    Quote:
      type: object
      required:
        - id
        - quote
      properties:
        id:
          type: integer
        quote:
          type: string

I only needed to make one manual updateā€”making sure theĀ idĀ andĀ quoteĀ properties wereĀ requiredĀ for theĀ QuoteĀ schema. And thatā€™s only because I forgot to mention this constraint to ChatGPT in my original prompt.

With that, weā€™re ready to develop the new service using anĀ API-FirstĀ approach.

Building the Spring Boot Service Using API-First

For this example, Iā€™ll use theĀ Spring Boot CLIĀ to create a new project. Hereā€™s how you can install the CLI using Homebrew:

$ brew tap spring-io/tap
$ brew install spring-boot

Create a new Spring Boot Service

Weā€™ll call the projectĀ quotes, creating it with the following command:

$ spring init --dependencies=web quotes

Letā€™s examine the contents of theĀ quotesĀ folder:

$ cd quotes && ls -la

total 72
drwxr-xr-x@ 11 jvester    352 Mar  1 10:57 .
drwxrwxrwx@ 90 jvester   2880 Mar  1 10:57 ..
-rw-r--r--@  1 jvester     54 Mar  1 10:57 .gitattributes
-rw-r--r--@  1 jvester    444 Mar  1 10:57 .gitignore
-rw-r--r--@  1 jvester    960 Mar  1 10:57 HELP.md
-rw-r--r--@  1 jvester    545 Mar  1 10:57 build.gradle
drwxr-xr-x@  3 jvester     96 Mar  1 10:57 gradle
-rwxr-xr-x@  1 jvester   8762 Mar  1 10:57 gradlew
-rw-r--r--@  1 jvester   2966 Mar  1 10:57 gradlew.bat
-rw-r--r--@  1 jvester     28 Mar  1 10:57 settings.gradle
drwxr-xr-x@  4 jvester    128 Mar  1 10:57 src

Next, we edit theĀ build.gradleĀ file as shown below to adopt the API-First approach.

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.3'
	id 'io.spring.dependency-management' version '1.1.7'
	id 'org.openapi.generator' version '7.12.0'
}

openApiGenerate {
    generatorName = "spring"
    inputSpec = "$rootDir/src/main/resources/static/openapi.yaml"
    outputDir = "$buildDir/generated"
    apiPackage = "com.example.api"
    modelPackage = "com.example.model"
    configOptions = [
            dateLibrary: "java8",
            interfaceOnly: "true",
            useSpringBoot3: "true",
            useBeanValidation: "true",
            skipDefaultInterface: "true"
    ]
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
       implementation 'io.swagger.core.v3:swagger-annotations:2.2.20'
    
       annotationProcessor 'org.projectlombok:lombok'
	
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

sourceSets {
	main {
		java {
			srcDirs += "$buildDir/generated/src/main/java"
		}
	}
}

compileJava.dependsOn tasks.openApiGenerate

tasks.named('test') {
	useJUnitPlatform()
}

Finally, we place the generated OpenAPI specification into theĀ resources/staticĀ folder asĀ openapi.yaml.

Generate the API and Model objects

After opening the project in IntelliJ, I executed the following command to build the API stubs and model objects.

./gradlew clean build

Now, we can see theĀ apiĀ andĀ modelĀ objects created from our OpenAPI specification. Hereā€™s theĀ QuotesAPI.javaĀ file:

Add the business logic

With the base service ready and already adhering to our OpenAPI contract, we start adding some business logic to the service.

First, we create aĀ QuotesRepositoryĀ class which returns the data for our service. As noted above, this would normally be stored in some dedicated persistence layer. For this example, hard-coding five quotesā€™ worth of data works just fine, and it keeps us focused.

@Repository
public class QuotesRepository {
    public static final List<Quote> QUOTES = List.of(
            new Quote()
                    .id(1)
                    .quote("The greatest glory in living lies not in never falling, but in rising every time we fall."),
            new Quote()
                    .id(2)
                    .quote("The way to get started is to quit talking and begin doing."),
            new Quote()
                    .id(3)
                    .quote("Your time is limited, so don't waste it living someone else's life."),
            new Quote()
                    .id(4)
                    .quote("If life were predictable it would cease to be life, and be without flavor."),
            new Quote()
                    .id(5)
                    .quote("If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success.")
    );

    public List<Quote> getAllQuotes() {
        return QUOTES;
    }

    public Optional<Quote> getQuoteById(Integer id) {
        return Optional.ofNullable(QUOTES.stream().filter(quote -> quote.getId().equals(id)).findFirst().orElse(null));
    }
}

Next, we create aĀ QuotesServiceĀ which will interact with theĀ QuotesRepository. Taking this approach will keep the data separate from the business logic.

@RequiredArgsConstructor
@Service
public class QuotesService {
    private final QuotesRepository quotesRepository;

    public List<Quote> getAllQuotes() {
        return quotesRepository.getAllQuotes();
    }

    public Optional<Quote> getQuoteById(Integer id) {
        return quotesRepository.getQuoteById(id);
    }

    public Quote getRandomQuote() {
        List<Quote> quotes = quotesRepository.getAllQuotes();
        return quotes.get(ThreadLocalRandom.current().nextInt(quotes.size()));
    }
}

Finally, we just need to implement theĀ QuotesApiĀ generated from our API-First approach:

@Controller
@RequiredArgsConstructor
public class QuotesController implements QuotesApi {
    private final QuotesService quotesService;

    @Override
    public ResponseEntity<List<Quote>> getAllQuotes() {
        return new ResponseEntity<>(quotesService.getAllQuotes(), HttpStatus.OK);
    }

    @Override
    public ResponseEntity<Quote> getQuoteById(Integer id) {
        return quotesService.getQuoteById(id)
                .map(quote -> new ResponseEntity<>(quote, HttpStatus.OK))
                .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @Override
    public ResponseEntity<Quote> getRandomQuote() {
        return new ResponseEntity<>(quotesService.getRandomQuote(), HttpStatus.OK);
    }
}

At this point, we have a fully-functional Motivational Quotes API, complete with a small collection of responses.

Some final items

Spring Boot gives us the option for a web-based Swagger Docs user interface via theĀ springdoc-openapi-starter-webmvc-uiĀ dependency.

dependencies {
	...
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5'
    ...
}

While the framework allows engineers to use simple annotations to describe their API, we can use our existingĀ openapi.yamlĀ file in theĀ resources/staticĀ folder.

We can implement this approach in theĀ application-properties.yamlĀ file, along with a few other minor configuration updates:

server:
  port: ${PORT:8080}
spring:
  application:
    name: quotes
springdoc:
  swagger-ui:
    path: /swagger-docs
    url: openapi.yaml

Just for fun, letā€™s add aĀ banner.txtĀ file for use when the service starts. We place this file into theĀ resourcesĀ folder.

${AnsiColor.BLUE}
                   _
  __ _ _   _  ___ | |_ ___  ___
 / _` | | | |/ _ \| __/ _ \/ __|
| (_| | |_| | (_) | ||  __/\__ \
 \__, |\__,_|\___/ \__\___||___/
    |_|
${AnsiColor.DEFAULT}
:: Running Spring Boot ${AnsiColor.BLUE}${spring-boot.version}${AnsiColor.DEFAULT} :: Port #${AnsiColor.BLUE}${server.port}${AnsiColor.DEFAULT} ::

Now, when we start the service locally, we can see the banner:

Once started, we can validate the Swagger Docs are working by visiting theĀ /swagger-docsĀ endpoint.

Finally, weā€™ll create a new Git-based repository so that we can track any future changes:

$ git init
$ git add .
$ git commit -m "Initial commit for the Motivational Quotes API"

Now,Ā letā€™s see how quickly we can deploy our service.

Using Heroku to Finish the Journey

So far, the primary focus for introducing my new idea has been creating an OpenAPI specification and writing some business logic for my service. Spring Boot handled everything else for me.

When it comes to running my service, I prefer to use Heroku because itā€™s a great fit for Spring Boot services. I can deploy my services quickly without getting bogged down with cloud infrastructure concerns. Heroku also makes it easy toĀ pass in configuration values for my Java-based applications.

To match the Java version weā€™re using, we create aĀ system.propertiesĀ file in the root folder of the project. The file has one line:

java.runtime.version = 17

Then, I create aĀ ProcfileĀ in the same location for customizing the deployment behavior. This file also has one line:

web: java -jar build/libs/quotes-0.0.1-SNAPSHOT.jar

Itā€™s time to deploy. With theĀ Heroku CLI, I can deploy the service using a few simple commands. First, I authenticate the CLI and then create a new Heroku app.

$ heroku login
$ heroku create

Creating app... done, vast-crag-43256
https://vast-crag-43256-bb5e35ea87de.herokuapp.com/ | https://git.heroku.com/vast-crag-43256.git

My Heroku app instance is namedĀ vast-crag-43256Ā (I could have passed in a specified name), and the service will run at https://vast-crag-43256-bb5e35ea87de.herokuapp.com/.

The last thing to do is deploy the service by using a Git command to push the code to Heroku:

$ git push heroku master

Once this command is complete, we can validate a successful deployment via the Heroku dashboard:

Now, weā€™re ready to take our new service for a test drive!

Motivational Quotes in Action

With the Motivational Quotes service running on Heroku, we can validate everything is working as expected using a series ofĀ curlĀ commands.

First, letā€™s get a complete list of all five motivational quotes:

$ curl \
  --location 'https://vast-crag-43256-bb5e35ea87de.herokuapp.com/quotes'

[
   {
      "id":1,
      "quote":"The greatest glory in living lies not in never falling, but in rising every time we fall."
   },
   {
      "id":2,
      "quote":"The way to get started is to quit talking and begin doing."
   },
   {
      "id":3,
      "quote":"Your time is limited, so don't waste it living someone else's life."
   },
   {
      "id":4,
      "quote":"If life were predictable it would cease to be life, and be without flavor."
   },
   {
      "id":5,
      "quote":"If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success."
   }
]

Letā€™s retrieve a single motivational quote by ID:

$ curl \
  --location 'https://vast-crag-43256-bb5e35ea87de.herokuapp.com/quotes/3'

{
   "id":3,
   "quote":"Your time is limited, so don't waste it living someone else's life."
}

Letā€™s get a random motivational quote:

$ curl --location \
  'https://vast-crag-43256-bb5e35ea87de.herokuapp.com/quotes/random'

{
   "id":5,
   "quote":"If you set your goals ridiculously high and it's a failure, you will fail above everyone else's success."
}

We can even browse the Swagger Docs too.

Conclusion

Time to market can make or break any idea. This is why startups are laser-focused on delivering their innovations as quickly as possible. The longer it takes to reach the finish line, the greater the risk of a competitor arriving before you.

My readers may recall my personal mission statement, which I feel can apply to any IT professional:

ā€œFocus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.ā€

ā€” J. Vester

In this article, we saw how Spring Boot handled everything required to implement a RESTful API. Leveraging ChatGPT, we were even able to express in human words what we wanted our service to be, and it created an OpenAPI specification for us in a matter of seconds. This allowed us to leverage an API-First approach. Once ready, we were able to deliver our idea using Heroku by issuing a few CLI commands.

Spring Boot, ChatGPT, and Heroku provided the frameworks and services so that I could remain laser-focused on realizing my idea. As a result, I was able to adhere to my personal mission statement and, more importantly, deliver my idea quickly. All I had to do was focus on the business logic behind my ideaā€”and thatā€™s the way it should be!

If youā€™re interested, the source code for this article can be found onĀ GitLab.

Have a really great day!


Written by johnjvester | Information Technology professional with 25+ years expertise in application design and architecture.
Published by HackerNoon on 2025/03/24