Spring Boot API Tutorial: Build, Document, and Secure a REST API
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. 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 and Python.
This time, we're diving into Java and the popular Spring Boot Framework 🤓
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?#
- Creating the project
- Adding the required packages
- Creating a MongoDB Atlas Account
- Creating the API
- Testing our API
- Hosting our API for the world to see
- Creating a project on Zuplo
- Adding Rate Limiting
- Setting Up API Key Authentication
- Configuring Geographic Request Filtering
- Developer Documentation Portal
- Wrapping Up
Creating the project#
Since we’re building a Spring Boot project, we’ll use spring initializer to get started.
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 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. It’s straightforward but here’s a little guide 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.
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:
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:
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:
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
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:
mvn spring-boot:run
Open your favorite web browser and navigate to http://localhost:8080/api/:
Success! It’s working as expected, although we have only tested the presentation endpoint 😅 Let's add some entries:
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."}'
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."}'
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:
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 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.
Let's choose CPU Eco and select whatever region is close to us.
Here is where we define our environment variables, so consider this a replacement for the .env file:
We're now ready to deploy our service:
And it should be ready. We need to copy the URL and open it on a browser.
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.
Once we're done, we can call up our service.
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
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.
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
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, 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.
Once the project is created, we’ll be greeted with this screen. From here, press Start Building:
To start enhancing our API, click on routes.oas.json:
Next, we need to add a route, which Zuplo will manage:
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.
All of our endpoints will be imported automagically 🤩
Nothing else to do here, everything is ready for testing.
Choose any endpoint like Get all posts, and press the Test button.
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.
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:
All we need to do here is press Ok. Easy, right?
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.
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.
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:
There’s not much to do here—just press OK.
It’s fundamental to move the api-key-inbound policy to the top:
If we save it and try testing the API, access will be denied:
At the top of the screen, click Services, then select Configure under API Key Service:
We need to create a consumer, which will function as a token:
We need to set a name for the subject and specify the email or emails associated with the key:
Once we click Save consumer, an API key will be generated. We can either view it or copy it:
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:
Success once again! It’s working as expected! 🥳🥳🥳
If you rather code it, then read this:
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.
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.
Developer Documentation Portal#
If we click on Gateway at the bottom of the screen, we’ll see a link to the Developer Portal:
Here, we can see that every route we create will be added to the Developer Portal:
Wrapping Up#
If you’d like to check the source code for the API, you can see it on my Github repo SimpleBlogAPI_Spring.
If you want to learn more about the tools I used - here are some quick documentation links:
Now, what kind of API will you build with Spring Boot? 😎