Home / Java & Spring Boot / Building the Contact Form API

Lesson 3 of 6

Building the Contact Form API

Estimated time: 2–2.5 hours

What You Will Learn

  • What a REST API is and how the HTTP methods (GET, POST, PUT, DELETE) work
  • How to create a data model (a ContactMessage class) in Java to represent a contact form submission
  • How to build API endpoints that can receive new messages and return a list of all messages
  • How to test your API using JavaScript fetch() from the browser
  • How to connect a front-end form to your Spring Boot backend
  • Why in-memory storage is not enough for a real application — and what to do about it

In Lesson 12, you created your first Spring Boot application and saw how Java can power a web server. You made a simple endpoint that returned a "Hello, world!" message. That was a great start, but real websites need to do much more than say hello. They need to receive data from users, process it, and send responses back.

In this lesson, we are going to build something genuinely useful: a REST API that handles contact form submissions. If you completed the Web Basics track, you may remember building a contact form with HTML. That form had fields for a name, email address, and message — but when you clicked "Submit," nothing actually happened on the server side. There was no backend to receive the data. Today, we build that backend.

By the end of this lesson, you will have a working API that can accept contact form submissions from a web browser and send back a list of all the messages it has received. This is the same pattern used by every website that has a contact page, a signup form, or a comment section. Let's get started.

1. What is a REST API?

Before we write any code, we need to understand what we are building. You have probably heard the term "API" before. API stands for Application Programming Interface. In simple terms, an API is a set of rules that allows one piece of software to talk to another piece of software. When your web browser needs data from a server, it uses an API to ask for it. When a mobile app loads your social media feed, it is calling an API behind the scenes.

The specific style of API we are going to build is called a REST API. REST stands for Representational State Transfer. That is a fancy name, but the idea is straightforward. A REST API uses standard web addresses (URLs) and standard HTTP methods to let clients (like web browsers or mobile apps) communicate with a server. Every time you type a URL into your browser's address bar and press Enter, you are making an HTTP request. A REST API simply formalizes how those requests and responses should be structured.

Think of it like a restaurant. You (the customer) sit at a table and look at the menu. When you are ready, you tell the waiter what you want. The waiter takes your order to the kitchen, the kitchen prepares your food, and the waiter brings it back to your table. In this analogy:

Just like a restaurant has different types of orders (dine-in, takeout, delivery), HTTP has different methods that describe what you want the server to do. There are four main ones you need to know:

In this lesson, we are going to focus on GET and POST because those are the two methods our contact form needs. We will use POST to send a new contact message to the server, and GET to retrieve a list of all the messages that have been submitted.

When a REST API sends data back and forth, it almost always uses a format called JSON. JSON stands for JavaScript Object Notation. It is a lightweight, human-readable way to represent data. Here is what a contact message looks like in JSON format:

{
  "id": 1,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "message": "Hello from the frontend!"
}

If you have worked with JavaScript objects before (from the Web Basics track), this format should look very familiar. JSON uses key-value pairs inside curly braces. The keys are always strings in double quotes, and the values can be strings, numbers, booleans, arrays, or other objects. JSON has become the standard format for web APIs because it is easy for both humans and machines to read and write.

One of the most powerful things about Spring Boot is that it handles JSON conversion automatically. When someone sends JSON data to your API, Spring Boot converts it into a Java object for you. When your API needs to send data back, Spring Boot converts your Java object into JSON. You do not have to write any parsing code yourself. This is a huge time-saver and one of the reasons Spring Boot is so popular for building APIs.

Now that you understand the concepts, let's start building. Our contact form API will have two endpoints:

2. Creating the Contact Message Model

Every application needs to define the shape of its data. When someone submits a contact form, what information do we need to capture? At a minimum, we want their name, their email address, and their message. We also want to give each submission a unique ID number so we can tell them apart.

In Java, we represent the shape of data using a class. If you recall from Lesson 12, a class is like a blueprint. It defines what an object looks like and what data it holds. We are going to create a class called ContactMessage that acts as the blueprint for a single contact form submission.

Think of it this way: if you were designing a paper form for people to fill out at a front desk, you would decide which fields appear on the form — Name, Email, Message. The ContactMessage class is the digital version of that paper form. Each time someone submits the form, we create a new instance (a filled-out copy) of that class.

Here is the complete class. Create a new file called ContactMessage.java in your project's source folder, right next to your main application class:

package com.example.contactapi;

public class ContactMessage {

    private Long id;
    private String name;
    private String email;
    private String message;

    // Default constructor (required by Spring Boot for JSON conversion)
    public ContactMessage() {
    }

    // Constructor with all fields
    public ContactMessage(Long id, String name, String email, String message) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.message = message;
    }

    // Getters
    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getMessage() {
        return message;
    }

    // Setters
    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

That looks like a lot of code, but it is actually very straightforward once you break it down. Let's walk through each part.

First, the fields (also called instance variables) at the top of the class:

Next, the constructors. A constructor is a special method that gets called when you create a new instance of the class. We have two:

Finally, the getters and setters. You might be wondering: why do we need these methods? Why can't we just access the fields directly? This is a core Java concept called encapsulation. By making the fields private and providing public getter and setter methods, we control how the data is accessed and modified. This has several benefits:

A getter is a method that returns the value of a field. By convention, it starts with get followed by the field name with a capital first letter. So the getter for name is getName(). A setter is a method that sets the value of a field. It starts with set and takes one argument. The setter for name is setName(String name).

If you completed the Web Basics track, think back to the contact form you built in HTML. That form had an <input> for the person's name, an <input> for their email, and a <textarea> for their message. The ContactMessage class is the server-side mirror of those form fields. Each field in the class corresponds to a field in the HTML form. When the browser sends the form data to our server as JSON, Spring Boot maps each JSON key to the matching setter method in our class.

With our data model in place, we are ready to build the part of the application that actually handles incoming requests.

3. Building the Controller

In Spring Boot, a controller is the class that handles incoming HTTP requests. It is the waiter from our restaurant analogy — the part of your application that listens for requests from clients, processes them, and sends back responses. Each method inside a controller corresponds to a specific URL and HTTP method.

We are going to create a controller called ContactController. This controller will have two endpoints:

For now, we are going to store messages in an ArrayList — a simple list that lives in the server's memory. This is the quickest way to get our API working. (We will discuss the drawback of this approach at the end of the lesson.)

Create a new file called ContactController.java in the same package as your main application class:

package com.example.contactapi;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class ContactController {

    // In-memory storage for contact messages
    private List<ContactMessage> messages = new ArrayList<>();

    // Counter to assign unique IDs to each message
    private long nextId = 1;

    // POST /api/contact — receive a new contact message
    @PostMapping("/api/contact")
    public ContactMessage createMessage(@RequestBody ContactMessage incoming) {
        incoming.setId(nextId);
        nextId++;
        messages.add(incoming);
        return incoming;
    }

    // GET /api/contact — return all contact messages
    @GetMapping("/api/contact")
    public List<ContactMessage> getAllMessages() {
        return messages;
    }
}

This is the heart of our API, so let's go through every piece carefully.

The @RestController annotation: At the very top of the class, we have @RestController. This is a special Spring Boot annotation that tells the framework: "This class is a controller, and every method in it should return data directly (not an HTML page)." When you see @RestController, it means the controller is designed for a REST API — it sends and receives JSON data.

The in-memory storage: Inside the class, we declare two fields. The first is messages, which is an ArrayList of ContactMessage objects. An ArrayList is Java's resizable array — you can keep adding items to it and it grows as needed. This is where we store every contact message that gets submitted. The second field is nextId, a counter that starts at 1. Every time a new message comes in, we assign it the current value of nextId and then increment the counter by one, ensuring every message gets a unique ID.

The @PostMapping method: The createMessage method is decorated with @PostMapping("/api/contact"). This tells Spring Boot: "When someone sends an HTTP POST request to the URL /api/contact, run this method." The method takes one parameter: @RequestBody ContactMessage incoming. This is where the magic happens.

The @RequestBody annotation tells Spring Boot: "Take the JSON data from the body of the HTTP request and convert it into a ContactMessage Java object." So when a browser sends JSON like {"name": "Jane", "email": "jane@example.com", "message": "Hello!"}, Spring Boot automatically creates a ContactMessage object, calls setName("Jane"), setEmail("jane@example.com"), and setMessage("Hello!"). You do not have to parse the JSON yourself. Spring Boot handles all of it.

Inside the method, we assign an ID to the message, add it to our list, and then return incoming. When a @RestController method returns a Java object, Spring Boot automatically converts it back into JSON and sends it in the HTTP response. So the client receives JSON confirmation of the message that was just saved, including the newly assigned ID.

The @GetMapping method: The getAllMessages method is decorated with @GetMapping("/api/contact"). This tells Spring Boot: "When someone sends an HTTP GET request to /api/contact, run this method." The method simply returns the entire messages list. Spring Boot converts the list of Java objects into a JSON array and sends it back to the client.

Notice how we use the same URL path (/api/contact) for both endpoints. This is perfectly fine because the HTTP method is different — one is POST and the other is GET. Spring Boot uses the combination of the URL path and the HTTP method to determine which controller method to call. This is standard REST API design: the URL represents the resource (contact messages), and the HTTP method represents the action (create or read).

Let's look at the flow of a complete request. When a user fills out a contact form on a website and clicks "Submit":

  1. The browser collects the form data (name, email, message) and packages it as JSON.
  2. The browser sends an HTTP POST request to http://localhost:8080/api/contact with the JSON in the request body.
  3. Spring Boot receives the request, sees that it matches @PostMapping("/api/contact"), and calls the createMessage method.
  4. The @RequestBody annotation triggers automatic JSON-to-Java conversion, creating a ContactMessage object.
  5. Our code assigns an ID, stores the message in the list, and returns the object.
  6. Spring Boot converts the returned object back to JSON and sends it as the HTTP response.
  7. The browser receives the JSON response and can display a success message to the user.

That entire round trip — from the user clicking "Submit" to seeing a confirmation — typically happens in a fraction of a second. And you just built the entire server side of it in about 30 lines of Java code. That is the power of Spring Boot.

Where do the files go? Both ContactMessage.java and ContactController.java should be in the same package as your main application class (the one with @SpringBootApplication). If your main class is in src/main/java/com/example/contactapi/, put both new files there too. Spring Boot automatically scans the package of your main class (and its sub-packages) for controllers and other components.

4. Testing Your API

You have written the model and the controller. Now it is time to see if everything works! Start your Spring Boot application the same way you did in Lesson 12: run the main class from your IDE or use ./mvnw spring-boot:run in your terminal. You should see the Spring Boot banner and a message saying the application started on port 8080.

Testing GET with Your Browser

The easiest test is to open your web browser and navigate to http://localhost:8080/api/contact. Since your browser sends a GET request by default, this will call your getAllMessages() method. Since no one has submitted any messages yet, you should see an empty JSON array:

[]

That empty pair of square brackets is valid JSON. It means "an empty list." This is exactly what we expect — our ArrayList starts out empty, so there are no messages to return. The API is working!

Testing POST with JavaScript fetch()

Testing a GET request is easy — just visit the URL. But testing a POST request is trickier because browsers do not send POST requests when you type a URL. We need to use JavaScript's fetch() function to send a POST request with JSON data.

You can test this right from your browser's developer console (press F12 and go to the Console tab), or you can use the runnable editor below. Important: your Spring Boot server must be running on localhost:8080 for this to work. If the server is not running, you will see an error message.

Try It Yourself

Make sure your Spring Boot application is running, then click "Run" to send a contact form submission to your API. Try changing the name, email, and message to your own information!

// Try running this while your Spring Boot server is running!
// This sends a contact form submission to your API.

fetch('http://localhost:8080/api/contact', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Jane Doe',
    email: 'jane@example.com',
    message: 'Hello from the frontend!'
  })
})
.then(response => response.json())
.then(data => console.log('Server responded:', data))
.catch(error => console.log('Error (is your server running?):', error.message));

Let's break down every part of that fetch() call so you understand exactly what is happening.

fetch('http://localhost:8080/api/contact', { ... }) — The fetch() function is built into every modern browser. It sends an HTTP request to the URL you provide. The first argument is the URL. The second argument is an options object that lets you configure the request.

method: 'POST' — This tells fetch() to send a POST request instead of the default GET. Remember, POST means "I want to create new data on the server."

headers: { 'Content-Type': 'application/json' } — HTTP headers are like labels on a package. This header tells the server: "The data I am sending is in JSON format." Without this header, Spring Boot would not know how to interpret the request body.

body: JSON.stringify({ name: 'Jane Doe', ... }) — The body is the actual data we are sending. JSON.stringify() takes a JavaScript object and converts it into a JSON string. This is the data that Spring Boot will receive, convert into a ContactMessage object using @RequestBody, and store in the list.

.then(response => response.json()) — The fetch() function returns a Promise (an object representing a future result). The first .then() takes the raw HTTP response and converts it to a JavaScript object by parsing the JSON.

.then(data => console.log('Server responded:', data)) — The second .then() receives the parsed data and prints it to the console. If everything worked, you should see the contact message with its newly assigned ID.

.catch(error => console.log('Error ...', error.message)) — The .catch() handles any errors that occur. The most common error is a network error, which usually means your Spring Boot server is not running.

Checking the Results

After running the fetch() code, go back to your browser and visit http://localhost:8080/api/contact again. Instead of an empty array, you should now see your message in the list:

[
  {
    "id": 1,
    "name": "Jane Doe",
    "email": "jane@example.com",
    "message": "Hello from the frontend!"
  }
]

It worked! Your API received the JSON data, converted it into a Java object, assigned it an ID, stored it in the list, and now it returns that list when you visit the URL. Try running the fetch() code a few more times with different names and messages, then refresh the GET endpoint. You should see multiple messages, each with a unique ID.

5. Connecting to Your Resume Site

Now that your API is working, you probably want to connect it to an actual HTML form. If you built a resume site or contact page during the Web Basics track, this is where those two worlds come together. You are going to add JavaScript to your contact form so that when a user clicks "Submit," it sends the form data to your Spring Boot API instead of just doing nothing.

Here is what the JavaScript on your contact page would look like. You would add this to the HTML file that contains your contact form, inside a <script> tag at the bottom of the page:

// Get the form element
const form = document.getElementById('contact-form');

// Listen for the form's submit event
form.addEventListener('submit', function(event) {
    // Prevent the default form submission (which would reload the page)
    event.preventDefault();

    // Collect the data from the form fields
    const formData = {
        name: document.getElementById('name').value,
        email: document.getElementById('email').value,
        message: document.getElementById('message').value
    };

    // Send the data to our Spring Boot API
    fetch('http://localhost:8080/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
    })
    .then(response => response.json())
    .then(data => {
        alert('Message sent! Thank you, ' + data.name + '!');
        form.reset(); // Clear the form fields
    })
    .catch(error => {
        alert('Something went wrong. Please try again.');
        console.log('Error:', error);
    });
});

This code uses addEventListener to listen for the form's submit event. When the user clicks the submit button, the function runs. The first thing it does is call event.preventDefault(), which stops the browser from performing its default form submission behavior (which would reload the page). Then it collects the values from each form field, packages them into a JavaScript object, and sends them to our API using fetch() — exactly like we did in the testing section.

The CORS Problem (and How to Fix It)

If your HTML file is not being served by Spring Boot itself (for example, if you open it directly from your file system or serve it from a different server), you will run into a problem called CORS. CORS stands for Cross-Origin Resource Sharing, and it is a security feature built into web browsers.

Here is the issue: browsers block web pages from making requests to a different "origin" (a different domain, port, or protocol) than the one the page came from. If your HTML form is at http://localhost:3000 (or opened as a file) and your API is at http://localhost:8080, the browser sees these as different origins and blocks the request.

The fix is simple. Add the @CrossOrigin annotation to your controller. This tells Spring Boot to include special headers in its responses that tell the browser: "It is okay for other origins to make requests to this API."

package com.example.contactapi;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@CrossOrigin(origins = "*")
@RestController
public class ContactController {

    private List<ContactMessage> messages = new ArrayList<>();
    private long nextId = 1;

    @PostMapping("/api/contact")
    public ContactMessage createMessage(@RequestBody ContactMessage incoming) {
        incoming.setId(nextId);
        nextId++;
        messages.add(incoming);
        return incoming;
    }

    @GetMapping("/api/contact")
    public List<ContactMessage> getAllMessages() {
        return messages;
    }
}

The only change is the addition of @CrossOrigin(origins = "*") right above @RestController. The * means "allow requests from any origin." In a real production application, you would replace the * with the specific URL of your frontend (like "https://mywebsite.com") for better security. But for development and learning, allowing all origins is perfectly fine.

With @CrossOrigin in place, your HTML contact form can now successfully communicate with your Spring Boot API, even if they are running on different ports or being served from different locations.

6. The Catch: Why In-Memory Storage Is Not Enough

Everything is working beautifully. Your API receives messages, stores them, and returns them. You have connected your front-end form to your back-end server. Time to celebrate, right? Well, almost. There is one big problem hiding in our code, and you can discover it yourself with a simple experiment.

Go to your terminal and stop your Spring Boot server (press Ctrl + C). Then start it again with ./mvnw spring-boot:run. Once it is running, open your browser and visit http://localhost:8080/api/contact.

What do you see? An empty array. Every single message you submitted is gone. All of them. Completely erased.

This happened because we stored our messages in an ArrayList, which lives in the computer's memory (also called RAM). Memory is volatile — it only keeps data while the program is running. The moment you stop the server, the ArrayList is destroyed along with everything it contained. When the server starts up again, it creates a brand new, empty ArrayList, and all the old data is lost forever.

This is like writing all your important notes on a whiteboard instead of in a notebook. The whiteboard works great while you are in the room, but the moment someone erases it (or in this case, the server restarts), everything is gone. A real application needs to write those notes in a permanent notebook — something that survives restarts, crashes, and even power outages.

In the world of software, that "permanent notebook" is called a database. A database stores data on your hard drive (or SSD) instead of in volatile memory. This means the data persists even when the server is not running. When your server starts back up, it reads the data from the database and picks up right where it left off.

Our in-memory ArrayList was a useful shortcut for learning. It let us focus on understanding REST APIs, controllers, and JSON without also having to learn database configuration at the same time. But now that you understand how the API works, it is time to add real persistence.

In the next lesson, we fix this problem for good. In Lesson 14, we will add a SQLite database to our Spring Boot application. Every contact message will be saved to a file on disk, and it will still be there even if you restart the server a hundred times. Your contact form API will finally be production-ready. See you there.

Test Your Knowledge

1. What HTTP method is used to send new data to a server?

Correct! POST is the HTTP method used to send new data to a server and ask it to create a new resource. In our contact form API, we use POST to submit a new contact message. GET is for reading data, and DELETE is for removing data.

2. What does @RequestBody do in a Spring controller?

That's right! The @RequestBody annotation tells Spring Boot to take the JSON data from the body of the incoming HTTP request and automatically convert it into a Java object. In our case, it converts the JSON containing name, email, and message into a ContactMessage object. Spring Boot uses the setter methods on the class to populate each field.

3. Why does the contact data disappear when you restart the server?

Correct! Our contact messages are stored in an ArrayList, which lives in the computer's memory (RAM). Memory is volatile — when the server stops, everything in memory is erased. When the server starts again, it creates a fresh, empty ArrayList. To keep data permanently, we need a database that saves data to disk, which is exactly what we will add in Lesson 14.

Finished this lesson?

← Previous: Intro to Spring Boot Next: Adding a Real Database →