REST APIs for Beginners: What I Learned While Building a Spring Boot Application
A practical beginner-friendly guide to REST API design, DTOs, JWT authentication, Spring Security, frontend integration, and the lessons I learned while building a real full-stack application.

REST APIs for Beginners: What I Learned While Building a Spring Boot Application
When I first started learning REST APIs, the idea seemed simple.
GET retrieves data.POST creates data.PUT updates data.PATCH changes part of a resource.DELETE removes data.
I understood those definitions.
But once I started building a real full-stack application, I realized that knowing HTTP methods is only the beginning.
A real REST API also involves:
API design
Data validation
DTOs
Authentication
Authorization
Database relationships
Error handling
Security
Frontend integration
Environment configuration
Deployment
I understood REST much better while building my Career Tracker, a full-stack application built with Spring Boot, React, Spring Security, JWT, JPA, and MySQL.
This article is not intended to be a complete specification of REST.
Instead, it is a practical introduction based on the things I learned while building an actual application.
If you're learning REST APIs from scratch, I hope this gives you a clearer picture of how the pieces fit together.
The Project Behind This Article
The project is called Career Tracker.
It is designed to help users organize their job-search journey.
Users can track:
Job applications
Application status
Interviews
Upcoming interview information
Notifications
Dashboard statistics
Career analytics
Profile information
Career progress
The backend is built with:
Java
Spring Boot
Spring Security
JWT
Spring Data JPA
Hibernate
MySQL
Maven
The frontend is built separately using React.
You can explore the source code while reading this article.
Backend Repository
https://github.com/Dev-Venom/career-tracker-api
Frontend Repository
https://github.com/Dev-Venom/career-tracker-ui
The idea behind sharing the repository is simple:
You do not have to only read about the concepts.
You can also open the actual code and see where controllers, services, repositories, DTOs, authentication filters, and entities are implemented.
What Is a REST API?
In simple terms, a REST API allows different applications to communicate through HTTP.
In my Career Tracker application, the React frontend does not directly access MySQL.
Instead, the architecture looks like this:
React Frontend
|
| HTTP Request
v
Spring Boot REST API
|
v
Service Layer
|
v
Repository Layer
|
v
MySQL Database
Suppose the user opens their profile.
React sends:
GET /users/me
Spring Boot receives the request.
The backend determines which user is authenticated, retrieves the necessary information, converts it into a response object, and sends JSON back to React.
A simplified response could look like:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"targetRole": "Java Full Stack Developer",
"experienceLevel": "Entry Level",
"activelyLooking": true
}
React receives that JSON and displays the profile.
That is the basic idea.
The frontend asks for something.
The backend processes the request.
The backend returns a predictable response.
Understanding HTTP Methods
HTTP methods communicate what we want to do with a resource.
For a job application resource, the API might look like this:
GET /applications/my
POST /applications
GET /applications/{id}
PUT /applications/{id}
PATCH /applications/{id}/status
DELETE /applications/{id}
Let's look at them individually.
GET — Retrieve Data
GET is used when we want to read something.
For example:
GET /applications/my
This retrieves the applications belonging to the currently authenticated user.
Another example:
GET /users/me
This retrieves the currently authenticated user's profile.
A GET request should normally not modify the resource.
POST — Create Something
POST is generally used when creating a new resource.
For example:
POST /applications
The request might contain:
{
"companyName": "Example Company",
"jobTitle": "Java Developer",
"location": "Bangalore",
"status": "APPLIED"
}
The backend validates the request, creates the application, saves it, and returns a response.
PUT — Update a Resource
PUT is commonly used when updating an existing resource.
For example:
PUT /users/me
In Career Tracker, this is used for updating profile information.
A request could contain:
{
"name": "John Doe",
"phone": "9876543210",
"location": "India",
"targetRole": "Java Full Stack Developer",
"experienceLevel": "Entry Level",
"preferredJobType": "Full Time",
"preferredLocation": "Remote",
"activelyLooking": true
}
PATCH — Update Only Part of Something
Sometimes we do not want to update the entire resource.
For example, when dragging an application across a Kanban board, maybe only the status changes.
Instead of resending the whole application, we can use:
PATCH /applications/{id}/status
with something like:
{
"status": "INTERVIEW"
}
That is a good example of a partial update.
DELETE — Remove Something
When a user deletes an application:
DELETE /applications/{id}
The backend removes the resource if the authenticated user has permission to do so.
REST Endpoints Should Represent Resources
One common beginner approach is to create endpoints like this:
/getApplications
/createApplication
/updateApplication
/deleteApplication
These endpoints may still work.
But REST APIs become easier to understand when URLs represent resources instead of actions.
Instead of:
/createApplication
use:
POST /applications
Instead of:
/deleteApplication
use:
DELETE /applications/{id}
The endpoint represents the resource:
/applications
and the HTTP method represents the operation.
That gives the API a much more predictable structure.
A Controller Should Not Do Everything
When building a small tutorial application, it can be tempting to put database access, validation, and business logic directly inside controllers.
That becomes difficult to maintain once the application grows.
Career Tracker follows a layered structure:
Controller
|
v
Service
|
v
Repository
|
v
Database
Each layer has its own responsibility.
Controller Layer
The controller focuses on HTTP.
It handles things such as:
Request mappings
Request bodies
Path variables
Authentication information
HTTP responses
For example:
GET /users/me
is mapped inside the User controller.
But the controller does not need to contain all the logic required to build the profile response.
It calls the service.
Service Layer
The service handles application logic.
For example, it may:
Find the authenticated user
Update profile information
Calculate career statistics
Convert entities into DTOs
Apply application rules
This keeps the controller smaller and easier to understand.
Repository Layer
The repository handles persistence.
Spring Data JPA allows methods such as:
findByEmail(...)
or:
findByUser_Id(...)
or:
findByUser_IdAndStatus(...)
Spring generates the underlying database queries.
This greatly reduces the amount of repetitive database code.
DTOs Changed How I Think About API Responses
One of the most important things I learned while building this project was the value of DTOs.
DTO means:
Data Transfer Object
Imagine this User entity:
User
├── id
├── name
├── email
├── password
├── role
├── phone
├── location
└── targetRole
If I directly return this entity from the API, the response could contain information that should never reach the client.
The biggest example is:
password
Even if the password is hashed using BCrypt, there is no reason to expose the hash.
Instead, the backend creates something like:
UserResponseDto
The flow becomes:
Database
|
v
User Entity
|
v
Service
|
v
UserResponseDto
|
v
JSON Response
The response DTO contains only the fields the frontend needs.
That creates a much safer and cleaner API.
DTOs Also Protect the API Contract
DTOs are not only about security.
They also help separate:
Database model
from:
Public API model
Suppose the database structure changes later.
If the frontend depends directly on the entity structure, that database change may immediately affect the API.
Using DTOs gives us another layer of control.
That was one of the moments where REST API development started to feel less like CRUD and more like application design.
Frontend and Backend Need a Clear Contract
I encountered a simple example of this while implementing the Profile feature.
Imagine Spring Boot returns:
{
"targetRole": "Java Full Stack Developer"
}
But React tries to access:
user.role
The backend works.
The frontend works.
The request may even return:
200 OK
But the UI still displays the wrong value.
Why?
Because the backend says:
targetRole
while the frontend expects:
role
The frontend should instead use:
user.targetRole
That might look like a tiny issue.
But it taught me an important lesson:
An API response is a contract between systems.
Both sides need to agree on:
Field names
Data types
Meanings
Required values
Optional values
Response structure
When that contract becomes inconsistent, integration bugs appear very quickly.
Naming Matters More Than It Seems
Another interesting issue happened with the word:
role
My User entity already had:
role
for authorization.
For example:
USER
ADMIN
But the Career Tracker profile also needed something representing the user's career goal.
For example:
Java Full Stack Developer
Frontend Developer
Backend Developer
It would have been easy to use the same field for both.
That would have been a bad design.
One value controls authorization.
The other describes career information.
The correct model became:
role
→ USER / ADMIN
targetRole
→ Java Full Stack Developer
This separation prevents profile editing from accidentally interfering with security.
Small naming choices can become important architecture decisions later.
Authentication Changes REST API Design
Before authentication, an API may simply look like:
GET /applications
But once multiple users are using the application, a much more important question appears:
Which applications should this user be allowed to access?
Career Tracker uses Spring Security and JWT authentication.
Some endpoints are public.
For example:
POST /users
for registration.
And:
POST /users/login
for login.
But user-specific endpoints require authentication.
Examples include:
GET /users/me
PUT /users/me
GET /applications/my
How JWT Authentication Works
A simplified login flow looks like:
User enters email + password
|
v
POST /users/login
|
v
Backend finds user
|
v
BCrypt verifies password
|
v
JWT is generated
|
v
Token returned to frontend
The frontend then includes that token when calling protected APIs.
For example:
Authorization: Bearer <token>
The backend processes the request through a JWT filter.
Request
|
v
JWT Filter
|
v
Validate Token
|
v
Spring Security
|
v
Authenticated Controller
If the token is valid, Spring Security knows which user made the request.
Why I Prefer /users/me for Profile APIs
Without authentication, a frontend might request:
GET /users/15
But if the user is already authenticated, the backend already has an identity available.
Career Tracker therefore uses:
GET /users/me
for loading the profile.
And:
PUT /users/me
for updating it.
Conceptually:
JWT
|
v
Authenticated Email
|
v
UserRepository
|
v
Current User
The frontend does not need to decide which user ID should be loaded.
The backend identifies the user from authentication.
This makes the profile API simpler and safer.
Authentication Is Not the Same as Authorization
This distinction is important.
Authentication answers:
Who are you?
Authorization answers:
What are you allowed to do?
A valid JWT proves the user is authenticated.
But the backend should still verify that the user is allowed to access a particular application or interview.
For example, imagine:
DELETE /applications/50
The backend should not delete application 50 only because the requester has a valid token.
It should also verify that application 50 belongs to that user.
This is where APIs move beyond simple CRUD.
Never Let Profile Data Control Security Roles
Another security-related lesson came from profile editing.
A user might be allowed to update:
name
phone
location
targetRole
preferredLocation
activelyLooking
But a normal profile endpoint should not allow something like:
{
"role": "ADMIN"
}
That field affects authorization.
Security-related properties should not be treated like normal profile data.
This is another reason why using dedicated request DTOs is valuable.
The request DTO defines exactly what the client is allowed to update.
Don't Store Counters You Can Calculate Reliably
Career Tracker's profile displays statistics such as:
Applications
Interviews
Offers
Hired
One possible design would be storing:
applicationCount
interviewCount
offerCount
hiredCount
inside the User table.
But that creates duplicate information.
Suppose:
Applications = 20
Then the user deletes one.
The application table now contains:
19
But what happens if the counter is not updated?
The User table could still say:
20
Now the system contains conflicting information.
Instead, Career Tracker calculates statistics from the application records.
Conceptually:
User
|
v
Applications
|
+--> INTERVIEW
|
+--> OFFER
|
+--> HIRED
|
+--> ACCEPTED
|
v
CareerStatsDto
The real application records remain the source of truth.
Validation Makes APIs More Predictable
A backend should not assume the frontend will always send correct data.
For example, registration should not allow:
Empty name
Invalid email
Empty password
Spring supports Jakarta Validation annotations such as:
@NotBlank
and:
@Email
Validation gives the API a clear boundary.
Invalid data can be rejected before it enters deeper layers of the application.
This is useful because the frontend is not the only possible API client.
Someone could also call the API through:
Postman
curl
Mobile app
Another frontend
Another service
The backend needs to protect itself regardless of where the request comes from.
Test APIs Before Connecting the Frontend
This workflow saved me a lot of debugging time.
Before connecting a REST endpoint to React, test it independently first.
For example, before integrating the Profile page, I tested:
GET /users/me
and:
PUT /users/me
using an API testing tool.
That allows me to separate two possibilities.
If the request already fails independently:
Backend problem
If it works independently but fails inside React:
Frontend integration problem
Possible frontend integration issues include:
Wrong endpoint
Missing JWT
Incorrect Axios configuration
Wrong field name
Wrong request payload
CORS
State management
This is a simple habit, but it makes full-stack debugging much easier.
CORS Becomes Important With Separate Frontend and Backend Apps
During local development, the frontend may run on:
http://localhost:5173
while Spring Boot runs on:
http://localhost:8080
Those are different origins.
The browser applies cross-origin security rules.
The backend therefore needs a CORS configuration telling the browser which frontend origins are allowed.
One thing I changed while preparing Career Tracker for production was moving CORS configuration away from permanently hardcoded production URLs.
Instead, production can use:
app.cors.allowed-origins=${CORS_ALLOWED_ORIGINS}
That means the hosting environment decides which frontend is trusted.
Local development can use localhost.
Production can use the deployed frontend domain.
The Java code does not need to change for every environment.
Never Commit Production Secrets
While preparing the backend for deployment, configuration became another important lesson.
A beginner project might start with something like:
spring.datasource.password=myPassword
jwt.secret=mySecret
That becomes dangerous once the repository is pushed publicly.
Instead, production configuration should use environment variables:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
jwt.secret=${JWT_SECRET}
The actual values live inside the hosting environment.
The repository only contains placeholders.
This gives us:
Local Development
|
+--> Local database
+--> Local JWT secret
and:
Production
|
+--> Cloud database
+--> Production JWT secret
Same application.
Different configuration.
Separate Local and Production Configuration
Spring profiles are useful for this.
For example:
application.properties
application-local.properties
application-prod.properties
Local configuration can contain:
Local database configuration
Local JWT settings
Local CORS origins
Development logging
Production configuration can use:
Environment variables
Cloud database
Production JWT secret
Production CORS origin
Reduced SQL logging
This avoids changing application code when moving from your laptop to production.
REST APIs Are More Than CRUD
CRUD is still one of the best places to start learning.
You should understand:
Create
Read
Update
Delete
But once I started building Career Tracker, REST API development expanded into:
HTTP
Controllers
Services
Repositories
DTOs
Validation
Database Relationships
Authentication
Authorization
JWT
Spring Security
CORS
Exception Handling
Frontend Integration
Environment Variables
Deployment
That was the point where the backend started feeling like an actual system rather than a collection of endpoints.
Common Mistakes I Would Avoid
Here are some mistakes I would pay attention to if I were starting again.
Returning entities directly
Use DTOs when exposing API data.
Returning sensitive information
Password hashes and internal security information do not belong in normal responses.
Putting all logic inside controllers
Use services for business logic.
Hardcoding secrets
Use environment variables.
Making every endpoint public
Only expose what actually needs to be accessible anonymously.
Trusting user IDs blindly
Use authentication and verify resource ownership.
Using one field for multiple meanings
Keep security concepts and domain concepts separate.
Ignoring validation
The backend should validate incoming data.
Using inconsistent names
Frontend and backend need the same API contract.
Creating unnecessary action-style endpoints
Prefer resource-oriented URLs and appropriate HTTP methods.
Connecting the frontend too early
Test the backend independently before debugging integration.
A REST API Learning Path I Would Recommend
If you're starting REST APIs today, I would learn them roughly in this order:
HTTP Basics
|
v
GET / POST / PUT / DELETE
|
v
Simple CRUD API
|
v
Database Integration
|
v
Spring Data JPA
|
v
DTOs
|
v
Validation
|
v
Exception Handling
|
v
Authentication
|
v
JWT
|
v
Spring Security
|
v
Authorization
|
v
Frontend Integration
|
v
Environment Variables
|
v
Deployment
Do not wait until you completely understand everything before building something.
Build a small part.
Test it.
Break it.
Understand why it broke.
Fix it.
Move forward.
That process teaches a lot.
How to Explore the Career Tracker Repository
If you want to connect the ideas in this article to actual code, you can explore the backend here:
Career Tracker Backend
https://github.com/Dev-Venom/career-tracker-api
The main structure looks like:
src/main/java/com/kumara/careertracker/
├── config/
├── controller/
├── dto/
├── entity/
├── enums/
├── repository/
├── service/
└── CareertrackerApplication.java
If you're a beginner, I recommend exploring it in this order.
1. Start With controller/
Look at the API mappings.
You will see how Java methods are connected to HTTP routes.
For example:
GET
POST
PUT
PATCH
DELETE
This is where an incoming HTTP request first enters the application logic.
2. Move to service/
Then follow the method called by the controller.
This shows where business logic lives.
Controller
|
v
Service
This is often where you will see:
User lookup
Data validation
DTO conversion
Status logic
Statistics calculations
3. Explore repository/
Next, see how the service interacts with the database.
Service
|
v
Repository
|
v
MySQL
Spring Data JPA repositories make many common database operations much easier.
4. Look at dto/
This is especially useful if you're trying to understand frontend/backend communication.
DTOs show:
What the API accepts
What the API returns
Compare the DTOs to the entities.
You will see that they do not always contain the same fields.
That difference is intentional.
5. Finally Explore config/
This package contains some of the more advanced parts of the project.
For example:
Spring Security
JWT utilities
JWT filter
CORS configuration
The authenticated request flow is roughly:
React
|
v
HTTP Request + JWT
|
v
JWT Filter
|
v
Spring Security
|
v
Controller
|
v
Service
|
v
Repository
|
v
Database
Once you understand that flow, Spring Security becomes much less mysterious.
The Frontend Is Available Too
The React frontend is separate from the Spring Boot backend.
You can explore it here:
Career Tracker Frontend
https://github.com/Dev-Venom/career-tracker-ui
The frontend consumes the REST APIs for:
Authentication
Dashboard
Applications
Kanban workflow
Interviews
Notifications
Analytics
Profile
The complete architecture is:
React
|
| REST / JSON
v
Spring Boot
|
| JPA / Hibernate
v
MySQL
Looking at both repositories together can help if you're trying to understand how modern frontend and backend applications communicate.
What Building This Project Changed for Me
Before building Career Tracker, REST APIs mostly meant this to me:
GET
POST
PUT
DELETE
Now I think about REST APIs differently.
An API is a contract.
The frontend needs predictable responses.
The backend needs clear responsibilities.
The database needs to remain trustworthy.
Authentication needs to correctly identify users.
Authorization needs to protect resources.
Sensitive information should never leave the backend unnecessarily.
Configuration should work safely across different environments.
And the API should remain understandable as the application grows.
I did not learn all of those lessons from reading one definition of REST.
I learned many of them because something broke while I was building.
Final Thoughts
If you're learning REST APIs right now, start small.
Build CRUD.
Then don't stop there.
Add a database.
Add DTOs.
Add validation.
Add authentication.
Protect some endpoints.
Connect a frontend.
Add proper error handling.
Move your secrets into environment variables.
Try deploying it.
You will run into problems.
That is part of the learning process.
When something goes wrong, trace the request:
Frontend
|
v
HTTP Request
|
v
Security
|
v
Controller
|
v
Service
|
v
Repository
|
v
Database
Ask yourself:
At which part of this flow did my expectation stop matching reality?
That question has helped me debug a lot of issues.
I'm still learning and improving Career Tracker, but building a real project has helped me understand REST APIs much better than learning the concepts only in isolation.
If you're currently learning Spring Boot or REST APIs, feel free to explore the project, experiment with it, fork it, and build your own version.
Backend
https://github.com/Dev-Venom/career-tracker-api
Frontend
https://github.com/Dev-Venom/career-tracker-ui
If this article helped you understand REST APIs a little better, I'd love to know what you're currently building.
Build. Break. Learn.
Real projects. Real learning. Real growth.
