---
title: "Spring Boot API Tutorial: Build, Document, and Secure a REST API"
description: "Learn how to build an API with Java and Spring Boot. Implement API rate limiting, API key authentication and build an API developer documentation portal."
canonicalUrl: "https://zuplo.com/blog/2025/04/07/java-spring-boot-rest-api-tutorial"
pageType: "blog"
date: "2025-04-07"
authors: "blag"
tags: "Java, API Tooling, Tutorial"
image: "https://cdn.zuplo.com/cdn-cgi/image/fit=crop,width=1200,height=630/www/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/image.png"
---
> This article is written by Alvaro, a member of the Zuplo community and
> longtime API builder. You can check out more of his work
> [here](https://medium.com/@atejada). All opinions expressed are his own.

If you're keeping up with Zuplo's blog—and you should, given its great
insights—you might have come across my previous posts on building APIs with
[Ruby](/learning-center/how-to-build-an-api-with-ruby-and-sinatra) and
[Python](/learning-center/flask-api-tutorial).

This time, we're diving into Java and the popular
[Spring Boot Framework](https://spring.io/projects/spring-boot) 🤓

Previously, we used quotes from Dune with MongoDB Atlas for a consumer API and
built a simple blog API using data classes—without a database. This time, we’re
combining both approaches to create a blog API backed by MongoDB Atlas.

We’ll build a straightforward Blog Post CRUD API, handling everything from
creating and reading posts to updating and deleting them. Let’s get started!

## What are we going to do today?

1. [Creating the project](#creating-the-project)
2. [Adding the required packages](#adding-the-required-packages)
3. [Creating a MongoDB Atlas Account](#creating-a-mongodb-atlas-account)
4. [Creating the API](#creating-the-api)
5. [Testing our API](#testing-our-api)
6. [Hosting our API for the world to see](#hosting-our-api-for-the-world-to-see)
7. [Creating a project on Zuplo](#creating-a-project-on-zuplo)
8. [Adding Rate Limiting](#adding-rate-limiting)
9. [Setting Up API Key Authentication](#setting-up-api-key-authentication)
10. [Configuring Geographic Request Filtering](#configuring-geographic-request-filtering)
11. [Developer Documentation Portal](#developer-documentation-portal)
12. [Wrapping Up](#wrapping-up)

## Creating the project

Since we’re building a Spring Boot project, we’ll use
[spring initializer](https://start.spring.io/) to get started.

![Spring intializer](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_00.png)

When we are done, we can press the **Generate** button to download the project
on a .zip file

We can then open it using [IntelliJ IDEA](https://www.jetbrains.com/idea/) or
the IDE of your choice.

## Adding the required packages

spring initializer will add most of the packages that we need for this project,
so instead of displaying the **pom.xml** file, we're going to show the packages
that we need to use:

```
org.springframework.boot --> spring-boot-starter-data-mongodb
org.springframework.boot --> spring-boot-starter-web
org.springframework.boot --> spring-boot-starter-test
org.springdoc --> springdoc-openapi-starter-webmvc-ui --> 2.5.0
com.jayway.jsonpath --> json-path
me.paulschwarz --> spring-dotenv --> 3.0.0
```

## Creating a MongoDB Atlas account

Create your MongoDB Atlas
[free account](https://account.mongodb.com/account/register). It’s
straightforward but here’s a little
[guide](https://www.mongodb.com/docs/guides/atlas/account/) just in case.

In the end, you’re going to receive a string like this one:

```
mongodb+srv://<USER>:<PASSWORD>@blagcluster.<URL>.mongodb.net/quotes?retryWrites=true&w=majority&appName=BlagCluster
```

We need to keep it safe as we're going to use it later on in our project.

Something important to keep in mind is that in order to access MongoDB Atlas
from our local machine, we need to authorize our IP Address. Otherwise, we will
have auth problems. Head to **network** and press **Add Current IP Address**.

![Add IP address](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_01.png)

## Creating the API

First things first, create a **.env** file on your project root with the
following information:

```
USERNAME=<YOUR_USERNAME>
PASSWORD=<YOUR_PASSWORD>
```

Then open **application.properties** which should be located on the
**src/main/java/resources** folder and add the following:

```
spring.data.mongodb.uri=mongodb+srv://${USERNAME:World}:${PASSWORD:World}@blagcluster.b9jxy.mongodb.net/posts
spring.data.mongodb.database=posts

# OpenAPI Config
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.version=1.0.0
springdoc.title=Blog Post API
springdoc.description=API for managing blog posts

```

This will connect our application with MongoDB and it will also generate the
Swagger or OpenAPI Spec.

We need to create a package named **model** and put it inside a file named
**BlogPost** with the following code:

```java
package com.blog.posts.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document("BlogPost")
public class BlogPost {
    @Id
    private String id;
    private String title;
    private String description;

    public BlogPost(String title, String description) {
        super();
        this.title = title;
        this.description = description;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}
```

This will create our database or collection inside MongoDB. We're providing
getter and setter functions to access our DB properties.

Now, we need to create a package named **posts** and put it inside a file named
**PostRepository** with the following code:

```java
package com.blog.posts.posts;

import org.springframework.data.mongodb.repository.MongoRepository;
import com.blog.posts.model.BlogPost;

public interface PostRepository extends MongoRepository<BlogPost, String> {

}
```

This is just an interface to be able to interact with our model.

Here's the meat of our API. Create a file named **BlogController** with the
following code:

```java
package com.blog.posts;

import com.blog.posts.model.BlogPost;
import com.blog.posts.posts.PostRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api")
@Tag(name = "Blog Posts", description = "Operations for blog posts")
public class BlogController {
    @Autowired
    private PostRepository blogPost;

    @GetMapping("/")
    public String welcome() {
        return "Welcome to the Simple Blog API!";
    }

    @Operation(summary = "Get all posts")
    @GetMapping("/posts")
    public List<BlogPost> getAllPosts() {
        return blogPost.findAll();
    }

    @Operation(summary = "Get a post")
    @GetMapping("/posts/{id}")
    public Optional<BlogPost> getPost(@PathVariable String id){
        return blogPost.findById(id);
    }

    @Operation(summary = "Create new post")
    @PostMapping("/posts")
    public BlogPost addPost(@RequestBody BlogPost post) {
        return blogPost.save(post);
    }

    @Operation(summary = "Delete post")
    @DeleteMapping("/posts/{id}")
    public ResponseEntity<?> deletePost(@PathVariable String id) {
        return blogPost.findById(id)
                .map(post -> {
                    blogPost.delete(post);
                    return ResponseEntity.ok().build();
                })
                .orElse(ResponseEntity.notFound().build());
    }

    @Operation(summary = "Update post")
    @PutMapping("/posts/{id}")
    public BlogPost updatePost(@PathVariable String id, @RequestBody BlogPost post) {
        BlogPost existingPost = blogPost.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found"));
        existingPost.setTitle(post.getTitle());
        existingPost.setDescription(post.getDescription());
        return blogPost.save(existingPost);
    }
}
```

We're defining endpoints to read, create, update, and delete posts, but we're
also adding Swagger information, which will help us generate our OpenAPI Spec
file.

And last but no least, the file that will allow us to run our application in the
first place, **PostsApplication**

```java
package com.blog.posts;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@SpringBootApplication
@EnableMongoRepositories
public class PostsApplication {

  public static void main(String[] args) {
    SpringApplication.run(PostsApplication.class, args);
  }
}
```

And our application should be ready to roll.

## Testing our API

To run our application, we can head to the terminal window and type the
following:

```bash
mvn spring-boot:run
```

![Running Spring boot](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_02.png)

Open your favorite web browser and navigate to **http://localhost:8080/api/**:

![Default spring page response](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_03.png)

Success! It’s working as expected, although we have only tested the presentation
endpoint 😅 Let's add some entries:

```bash
curl -X POST "http://127.0.0.1:8080/api/posts" \
-H "Content-Type: application/json" \
-d '{"title": "Zuplo and Ruby," "description": "Zuplo, Ruby, and Sinatra."}'
```

```bash
curl -X POST "http://127.0.0.1:8080/api/posts" \
-H "Content-Type: application/json" \
-d '{"title": "Zuplo and Python," "description": "Zuplo, Python, and Flask."}'
```

```bash
curl -X POST "http://127.0.0.1:8080/api/posts" \
-H "Content-Type: application/json" \
-d '{"title": "Zuplo and Java," "description": "Zuplo, Java, and Spring."}'
```

We should have three new entries, let's check them out.

Navigate to **http://localhost:8080/api/posts**:

![Posts response](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_04.png)

Great news! It works as expected, but only on our local machine. Wouldn’t it be
amazing to share it with the world?

First, we need to upload our project to GitHub.

But before we do that, we need to change something on our
**application.properties** file:

```
#Use this locally
#spring.data.mongodb.uri=mongodb+srv://${USERNAME:World}:${PASSWORD:World}@blagcluster.b9jxy.mongodb.net/posts
#Use this for Koyeb
spring.data.mongodb.uri=mongodb+srv://${USERNAME}:${PASSWORD}@blagcluster.b9jxy.mongodb.net/posts
```

On Koyeb, we cannot use the **.env** file as we did locally, and it should not
be uploaded to Github either. So, you'd better add it to your **.gitignore**
file.

## Hosting our API for the world to see

There are plenty of options available for hosting an API. We’ll use
[Koyeb](https://www.koyeb.com/) for this particular API.

We want to create a new web service and select the repository we want to use—in
this case, **SimpleBlogAPI_Spring**.

![Choose github](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_05.png)

![Create a new service](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_06.png)

Let's choose **CPU Eco** and select whatever region is close to us.

![Choose CPU Eco](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_07.png)

Here is where we define our environment variables, so consider this a
replacement for the **.env** file:

![Setting environment variable](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_08.png)

We're now ready to deploy our service:

![Deploying to Koyeb](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_09.png)

And it should be ready. We need to copy the URL and open it on a browser.

![API deployed](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_10.png)

Although it will not work, for the same reason, our local host didn't work at
first. We need to add Koyeb's Outbound IP Address to the **Network** section of
MongoDB Atlas.

To do this, we need to run either of the following commands on the terminal
window, depending on the region we choose.

```
dig all-was1.infra.prod.koyeb.com

dig all-fra1.infra.prod.koyeb.com
```

Copy each IP Address from the response, and add it to the **network** section.

![Add IP access list entry](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_11.png)

Once we're done, we can call up our service.

![Remote hosted response](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_12.png)

As we have created our API with Swagger UI, we can take advantage of it 😎

In our browser, we need to navigate to:

```
https://modest-leonie-atejada-c8e59588.koyeb.app/swagger-ui/index.html
```

![Swagger UI](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_13.png)

It might not look helpful, but if you click the arrow on the right-hand side,
you can expand the endpoints and see everything in action.

![Playing with Swagger UI](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_14.png)

But that's not all we can do, we can also download this as a file and reuse it
later.

```
https://modest-leonie-atejada-c8e59588.koyeb.app/api-docs
```

![Accessing the raw OpenAPI file](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_15.png)

Download the file using **.json** and keep it safe.

Are we done? Yes and no. If we’re satisfied with something simple, unsecured,
and somehow rough around the edges, then sure, we’re good. But if we want to
improve it with minimal effort, we can bring in Zuplo and take things up a
notch.

## Creating a project on Zuplo

After creating a
[Zuplo account](https://portal.zuplo.com/signup?utm_source=blog), we’ll need to
set up a new project. We’ll name it **simple-blog-api-spring**, select an empty
project, and then create it. Wondering why Zuplo? Imagine building rate limits,
authentication, monetization, and other features entirely on your own. That’s a
lot of work and hassle. Zuplo simplifies all of that, making it a breeze.

![Create a Zuplo project](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_17.png)

Once the project is created, we’ll be greeted with this screen. From here, press
**Start Building**:

![Start building](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_18.png)

To start enhancing our API, click on **routes.oas.json**:

![routes.oas.json](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_19.png)

Next, we need to add a route, which Zuplo will manage:

![OpenAPI designer](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_20.png)

In the Ruby post, we went with **Add Route**, but in the Python post, we
selected **Import OpenAPI** instead, so let's do the same here. Now, you might
be wondering—how exactly do we do that? No worries, it’s easier than you think.
🤓

Let's upload the OpenAPI file that we downloaded earlier.

![Import OpenAPI](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_21.png)

All of our endpoints will be imported automagically 🤩

![OpenAPI imported](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_22.png)

Nothing else to do here, everything is ready for testing.

![View routes](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_23.png)

Choose any endpoint like **Get all posts**, and press the **Test** button.

![Rest endpoints](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_24.png)

Success! 🥳🥳🥳 Our Zuplo integration is working perfectly!

## Adding Rate Limiting

We don’t want people abusing our API or attempting to take down our server with
DDoS attacks. While coding a rate limit policy isn’t difficult, there are many
factors to consider—so why bother? Let’s have Zuplo manage that for us.

![Add inbound policy](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_25.png)

We need to add a new policy on the request side. Since we’ll be dealing with
many policies, simply type **rate** and select **Rate Limiting**:

![Choosing rate limit policy](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_26.png)

All we need to do here is press **Ok**. Easy, right?

![Applying the rate limiting policy](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_27.png)

Our rate limit has been added. Now, we need to save and run the test three
times. On the third attempt, we’ll hit the rate limit. Of course, we can adjust
this in the policy, as shown in the image above, where **requestsAllowed** is
set to 2. We only need to save for the change to be applied.

![Save the policy](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_28.png)

We can do that for all the other endpoints if we want to.

Exceeding the request limit will temporarily block further data requests. We’ll
need to wait a minute before trying again.

![Rate limit response](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_29.png)

So far, so good—but what if we want to prevent unauthorized access to our API?
We’ll need to implement some degree of authentication. While it’s not overly
complicated to build, it involves multiple steps, configurations, and testing.
Wouldn’t it be great to let Zuplo handle those details for us? That way, we can
focus solely on our API.

## Setting Up API Key Authentication

We need to navigate back to the **Policies** section and add a new policy—this
time, **API Key Authentication**:

![API key authentication policy](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_30.png)

There’s not much to do here—just press **OK**.

![API key auth policy configuration](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_31.png)

It’s fundamental to move the **api-key-inbound** policy to the top:

![Move API key policy first](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_32.png)

If we save it and try testing the API, access will be denied:

![Unauthorized response](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_33.png)

At the top of the screen, click **Services**, then select **Configure** under
**API Key Service**:

![Services tab](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_34.png)

We need to create a consumer, which will function as a token:

![Creating a consumer](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_35.png)

We need to set a name for the subject and specify the email or emails associated
with the key:

![Configuring a consumer](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_36.png)

Once we click **Save consumer**, an API key will be generated. We can either
view it or copy it:

![Save the consumer](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_37.png)

Now, we can go back to **Code** → **routes.oas.json** to test our API. Here, we
need to add the authorization header and pass the **Bearer** token along with
the API key:

![Calling the API with an API key](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_38.png)

Success once again! It’s working as expected! 🥳🥳🥳

If you rather code it, then read this:

![API key API](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_39.png)

And that’s how you enhance your API with Zuplo 😎 But wait, there’s more!

## Configuring Geographic Request Filtering

**Zuplo** provides many interesting policies, so I wanted to test that I have
never tested before, and Geo Location Inbound looked interesting enough.

Let's say we want to restrict access to our API from a particular country,
province, or even internet provider.

Living in Lima, Perú, I will restrict myself 🤣 and my Internet provider.

Look for **Geo-location filtering** and add it.

![Geo-filtering policy](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_40.png)

Let's change the **options** section with this:

```
{
  "export": "GeoFilterInboundPolicy",
  "module": "$import(@zuplo/runtime)",
  "options": {
    "block": {
      "asns": "AS6147",
      "countries": "PE",
      "regionCodes": "LIM"
    },
    "ignoreUnknown": true
  }
}
```

Save it and we can test it.

![Block Peru](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_41.png)

## Developer Documentation Portal

If we click on **Gateway** at the bottom of the screen, we’ll see a link to the
Developer Portal:

![Developer portal link](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_42.png)

Here, we can see that every route we create will be added to the Developer
Portal:

![API developer portal](/media/posts/2025-04-07-java-spring-boot-rest-api-tutorial/SpringZuplo_43.png)

## Wrapping Up

If you’d like to check the source code for the API, you can see it on my Github
repo [SimpleBlogAPI_Spring](https://github.com/atejada/SimpleBlogAPI_Spring).

If you want to learn more about the tools I used - here are some quick
documentation links:

- [Spring Boot](https://docs.spring.io/spring-boot/index.html)
- [Koyeb](https://www.koyeb.com/docs)
- [MongoDB Atlas](https://www.mongodb.com/docs/guides/atlas/account/)
- [Zuplo](https://zuplo.com/docs/articles/what-is-zuplo)

Now, what kind of API will you build with Spring Boot? 😎