
If you are exploring a java full stack course in Bangalore with placement, building a complete application is one of the best ways to apply what you learn. In this hands-on tutorial, we will create a task manager using Spring Boot, React, and PostgreSQL.
You will build a working application that can:
- Create tasks.
- Display tasks.
- Mark tasks as completed.
- Delete tasks.
- Validate user input.
- Store data in PostgreSQL.
- Connect a React frontend to a Java backend.
The project is small enough to complete in a weekend but includes concepts used in real Java full-stack development.
Project Architecture
The application will follow this structure:
React frontend
|
| HTTP requests
v
Spring Boot REST API
|
| Spring Data JPA
v
PostgreSQL database
The backend will use a layered architecture:
Controller
|
Service
|
Repository
|
Database
Each layer has a separate responsibility:
- The controller handles HTTP requests.
- The service contains business logic.
- The repository communicates with the database.
- The entity represents database data.
The final project structure will look like this:
task-manager/
├── backend/
│ └── src/main/java/com/example/taskmanager/
│ ├── controller/
│ ├── exception/
│ ├── model/
│ ├── repository/
│ └── service/
└── frontend/
├── src/
│ ├── App.jsx
│ └── App.css
└── package.json
Prerequisites
You should have basic knowledge of:
- Java classes and methods.
- JavaScript functions.
- HTML and CSS.
- SQL.
- HTTP requests.
Install these tools:
- JDK 17 or later.
- Maven.
- PostgreSQL.
- Node.js and npm.
- Git.
- An IDE such as IntelliJ IDEA or VS Code.
- Postman or
curl.
We will use:
- Java 17.
- Spring Boot 3.x.
- Spring Data JPA.
- PostgreSQL.
- React with Vite.
- Maven.
Check whether the tools are installed:
java -version
mvn -version
node --version
npm --version
psql --version
If each command returns version information, your environment is ready.
Step 1: Create the PostgreSQL Database
Start PostgreSQL and open the PostgreSQL command-line client:
psql -U postgres
Create a database named task_manager:
CREATE DATABASE task_manager;
Exit the PostgreSQL shell:
\q
PostgreSQL requires the server to be running before you create or access a database. The CREATE DATABASE command is the standard way to create a new database.
You do not need to create the tasks table manually. Hibernate will create it when the Spring Boot application starts.
Step 2: Create the Spring Boot Project
Create a Spring Boot project with these dependencies:
- Spring Web.
- Spring Data JPA.
- PostgreSQL Driver.
- Validation.
- Spring Boot DevTools.
You can create the project using an IDE or Spring Initializr.
Place the backend inside a directory named:
backend
The main application class should look like this:
package com.example.taskmanager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TaskManagerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskManagerApplication.class, args);
}
}
The @SpringBootApplication annotation enables component scanning, auto-configuration, and Spring Boot configuration.
Step 3: Configure the Database Connection
Open this file:
backend/src/main/resources/application.properties
Add the following configuration:
spring.datasource.url=jdbc:postgresql://localhost:5432/task_manager
spring.datasource.username=postgres
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
server.port=8080
Replace your_password with your local PostgreSQL password.
The important properties are:
-
spring.datasource.urltells Spring where PostgreSQL is running. -
spring.datasource.usernamespecifies the database user. -
spring.datasource.passwordspecifies the database password. -
spring.jpa.hibernate.ddl-auto=updateallows Hibernate to create or update tables during local development. -
server.port=8080runs the backend on port 8080.
For a production project, do not store credentials directly in the properties file. Use environment variables instead:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
On Linux or macOS:
export DB_URL=jdbc:postgresql://localhost:5432/task_manager
export DB_USERNAME=postgres
export DB_PASSWORD=your_password
On Windows PowerShell:
$env:DB_URL="jdbc:postgresql://localhost:5432/task_manager"
$env:DB_USERNAME="postgres"
$env:DB_PASSWORD="your_password"
Step 4: Create the Task Entity
Create this file:
backend/src/main/java/com/example/taskmanager/model/Task.java
Add the following code:
package com.example.taskmanager.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "tasks")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Title is required")
@Size(max = 120, message = "Title cannot exceed 120 characters")
@Column(nullable = false, length = 120)
private String title;
@Size(max = 500, message = "Description cannot exceed 500 characters")
private String description;
@Column(nullable = false)
private boolean completed = false;
public Task() {
}
public Task(String title, String description) {
this.title = title;
this.description = description;
this.completed = false;
}
public Long getId() {
return 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;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
}
Understanding the Entity
The @Entity annotation tells JPA that this class represents a database table.
The @Table(name = "tasks") annotation sets the table name.
The @Id annotation marks the primary key.
The @GeneratedValue annotation allows the database to generate the ID automatically.
The validation annotations protect your API from invalid input:
@NotBlank
prevents an empty title.
@Size(max = 120)
prevents titles longer than 120 characters.
The completed field defaults to false, so every new task starts as incomplete.
Step 5: Create the Repository
Create:
backend/src/main/java/com/example/taskmanager/repository/TaskRepository.java
package com.example.taskmanager.repository;
import com.example.taskmanager.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TaskRepository extends JpaRepository<Task, Long> {
}
By extending JpaRepository, your repository automatically receives common database methods:
findAll()
findById(id)
save(task)
deleteById(id)
existsById(id)
You do not need to write SQL for these basic operations.
You can also create custom query methods. For example:
import java.util.List;
List<Task> findByCompleted(boolean completed);
Spring Data JPA can create the query based on the method name.
Step 6: Create the Service Layer
Create:
backend/src/main/java/com/example/taskmanager/service/TaskService.java
package com.example.taskmanager.service;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.repository.TaskRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TaskService {
private final TaskRepository taskRepository;
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
public List<Task> getAllTasks() {
return taskRepository.findAll();
}
public Task getTaskById(Long id) {
return taskRepository.findById(id)
.orElseThrow(() ->
new RuntimeException("Task not found with id: " + id));
}
public Task createTask(Task task) {
return taskRepository.save(task);
}
public Task updateTask(Long id, Task updatedTask) {
Task existingTask = getTaskById(id);
existingTask.setTitle(updatedTask.getTitle());
existingTask.setDescription(updatedTask.getDescription());
existingTask.setCompleted(updatedTask.isCompleted());
return taskRepository.save(existingTask);
}
public Task toggleTask(Long id) {
Task task = getTaskById(id);
task.setCompleted(!task.isCompleted());
return taskRepository.save(task);
}
public void deleteTask(Long id) {
if (!taskRepository.existsById(id)) {
throw new RuntimeException("Task not found with id: " + id);
}
taskRepository.deleteById(id);
}
}
Why Use a Service Layer?
You could call the repository directly from the controller, but that approach becomes difficult to maintain.
The service layer is useful for:
- Business rules.
- Data validation.
- Transactions.
- Logging.
- Permission checks.
- Reusable application logic.
- Unit testing.
For example, if you later decide that completed tasks cannot be deleted, that rule belongs in the service layer.
Step 7: Create the REST Controller
Create:
backend/src/main/java/com/example/taskmanager/controller/TaskController.java
package com.example.taskmanager.controller;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.service.TaskService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
@CrossOrigin(origins = "http://localhost:5173")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping
public List<Task> getAllTasks() {
return taskService.getAllTasks();
}
@GetMapping("/{id}")
public Task getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Task createTask(@Valid @RequestBody Task task) {
return taskService.createTask(task);
}
@PutMapping("/{id}")
public Task updateTask(
@PathVariable Long id,
@Valid @RequestBody Task task
) {
return taskService.updateTask(id, task);
}
@PatchMapping("/{id}/toggle")
public Task toggleTask(@PathVariable Long id) {
return taskService.toggleTask(id);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
}
}
The controller exposes these endpoints:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks |
Get all tasks |
| GET | /api/tasks/{id} |
Get one task |
| POST | /api/tasks |
Create a task |
| PUT | /api/tasks/{id} |
Update a task |
| PATCH | /api/tasks/{id}/toggle |
Change completion status |
| DELETE | /api/tasks/{id} |
Delete a task |
The @CrossOrigin annotation allows the React application running on port 5173 to call the backend running on port 8080.
Step 8: Start and Test the Backend
Start the Spring Boot application from the backend directory:
./mvnw spring-boot:run
On Windows:
mvnw.cmd spring-boot:run
You can also start it with Maven:
mvn spring-boot:run
If the application starts successfully, you should see a message indicating that the server is running on port 8080.
Create a Task
Use curl to create a task:
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Learn Spring Boot","description":"Build a REST API"}'
Expected response:
{
"id": 1,
"title": "Learn Spring Boot",
"description": "Build a REST API",
"completed": false
}
Get All Tasks
curl http://localhost:8080/api/tasks
Get One Task
curl http://localhost:8080/api/tasks/1
Toggle Completion
curl -X PATCH http://localhost:8080/api/tasks/1/toggle
Delete a Task
curl -X DELETE http://localhost:8080/api/tasks/1
Test Validation
Send an empty title:
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"","description":"Invalid task"}'
Spring will reject the request because the title is required.
Step 9: Improve Error Handling
Returning a generic RuntimeException is not ideal. A REST API should return meaningful status codes and messages.
Create this exception class:
backend/src/main/java/com/example/taskmanager/exception/TaskNotFoundException.java
package com.example.taskmanager.exception;
public class TaskNotFoundException extends RuntimeException {
public TaskNotFoundException(Long id) {
super("Task not found with id: " + id);
}
}
Update the service imports:
import com.example.taskmanager.exception.TaskNotFoundException;
Change the getTaskById method:
public Task getTaskById(Long id) {
return taskRepository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
}
Update the deleteTask method:
public void deleteTask(Long id) {
if (!taskRepository.existsById(id)) {
throw new TaskNotFoundException(id);
}
taskRepository.deleteById(id);
}
Now create the global exception handler:
backend/src/main/java/com/example/taskmanager/exception/GlobalExceptionHandler.java
package com.example.taskmanager.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<Map<String, String>> handleTaskNotFound(
TaskNotFoundException exception
) {
Map<String, String> response = new HashMap<>();
response.put("error", exception.getMessage());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException exception
) {
Map<String, String> errors = new HashMap<>();
exception.getBindingResult()
.getFieldErrors()
.forEach(error ->
errors.put(
error.getField(),
error.getDefaultMessage()
)
);
return ResponseEntity
.badRequest()
.body(errors);
}
}
Now, requesting a task that does not exist returns:
{
"error": "Task not found with id: 99"
}
An invalid title returns:
{
"title": "Title is required"
}
This makes the API easier for the frontend and other clients to understand.
Step 10: Create the React Frontend
From the project root, create a React application with Vite:
npm create vite@latest frontend -- --template react
Move into the frontend directory:
cd frontend
Install dependencies:
npm install
Start the development server:
npm run dev
The frontend normally runs at:
http://localhost:5173
Create the React Application
Replace the contents of src/App.jsx with:
import { useEffect, useState } from "react";
import "./App.css";
const API_URL = "http://localhost:8080/api/tasks";
function App() {
const [tasks, setTasks] = useState([]);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [error, setError] = useState("");
async function loadTasks() {
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("Unable to load tasks");
}
const data = await response.json();
setTasks(data);
} catch (error) {
setError(error.message);
}
}
useEffect(() => {
loadTasks();
}, []);
async function addTask(event) {
event.preventDefault();
setError("");
if (!title.trim()) {
setError("Title is required");
return;
}
try {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
description,
}),
});
if (!response.ok) {
throw new Error("Unable to create task");
}
const newTask = await response.json();
setTasks((currentTasks) => [
...currentTasks,
newTask,
]);
setTitle("");
setDescription("");
} catch (error) {
setError(error.message);
}
}
async function toggleTask(id) {
try {
const response = await fetch(`${API_URL}/${id}/toggle`, {
method: "PATCH",
});
if (!response.ok) {
throw new Error("Unable to update task");
}
const updatedTask = await response.json();
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === updatedTask.id
? updatedTask
: task
)
);
} catch (error) {
setError(error.message);
}
}
async function deleteTask(id) {
try {
const response = await fetch(`${API_URL}/${id}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Unable to delete task");
}
setTasks((currentTasks) =>
currentTasks.filter((task) => task.id !== id)
);
} catch (error) {
setError(error.message);
}
}
return (
<main className="container">
<h1>Task Manager</h1>
<form onSubmit={addTask} className="task-form">
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Task title"
maxLength="120"
/>
<textarea
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="Description"
maxLength="500"
/>
<button type="submit">Add task</button>
</form>
{error && <p className="error">{error}</p>}
<section>
{tasks.length === 0 ? (
<p>No tasks yet.</p>
) : (
tasks.map((task) => (
<article key={task.id} className="task-card">
<div>
<h2 className={task.completed ? "completed" : ""}>
{task.title}
</h2>
<p>{task.description}</p>
</div>
<div className="actions">
<button onClick={() => toggleTask(task.id)}>
{task.completed
? "Mark pending"
: "Complete"}
</button>
<button onClick={() => deleteTask(task.id)}>
Delete
</button>
</div>
</article>
))
)}
</section>
</main>
);
}
export default App;
The useState hook stores component data such as tasks and form values. The useEffect hook loads tasks when the component is rendered.
Add CSS
Replace src/App.css with:
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f4f6f8;
color: #1f2937;
}
.container {
width: min(760px, 92%);
margin: 40px auto;
}
.task-form,
.task-card {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
}
.task-form {
display: grid;
gap: 12px;
}
input,
textarea,
button {
font: inherit;
padding: 10px;
}
textarea {
min-height: 90px;
resize: vertical;
}
button {
cursor: pointer;
border: 0;
border-radius: 6px;
background: #2563eb;
color: white;
}
.task-card {
display: flex;
justify-content: space-between;
gap: 16px;
}
.completed {
text-decoration: line-through;
color: #6b7280;
}
.actions {
display: flex;
gap: 8px;
align-items: center;
}
.error {
color: #b91c1c;
}
Open the frontend in your browser:
http://localhost:5173
Try the following:
- Add a task.
- Refresh the page.
- Mark the task as completed.
- Refresh the page again.
- Delete the task.
The data remains available after refreshing because it is stored in PostgreSQL rather than only in React state.
Step 11: Add the Project to Git
From the project root:
git init
git add .
git commit -m "Build task manager with Spring Boot and React"
Create a remote Git repository and connect it:
git branch -M main
git remote add origin your-repository-address
git push -u origin main
Before pushing, create a .gitignore file:
# Java
target/
*.class
# Node
node_modules/
dist/
# IDE
.idea/
.vscode/
# Environment files
.env
.env.*
Your README should contain:
- Project description.
- Features.
- Technologies used.
- Installation instructions.
- Database setup.
- API endpoints.
- Screenshots.
- Future improvements.
Useful startup commands:
# Start backend
cd backend
./mvnw spring-boot:run
# Start frontend
cd frontend
npm install
npm run dev
Never commit:
- Database passwords.
- API keys.
- Private tokens.
- Production environment files.
- Personal credentials.
Practical Exercises
After completing the basic application, extend it with these exercises.
Exercise 1: Add Due Dates
Add a due date field to the entity:
private LocalDate dueDate;
Then add the required getter and setter.
Think about these questions:
- Should the due date be optional?
- Should past due dates be allowed?
- How should the frontend display the date?
- Should overdue tasks have a different color?
Exercise 2: Add Task Filtering
Add an endpoint that filters tasks by completion status:
GET /api/tasks?completed=true
Add this method to the repository:
List<Task> findByCompleted(boolean completed);
Then update the service and controller to use the query parameter.
Exercise 3: Add Pagination
Returning every task works for a small project, but it becomes inefficient as the number of tasks grows.
Update the repository:
Page<Task> findAll(Pageable pageable);
Then support requests such as:
GET /api/tasks?page=0&size=10
Pagination reduces response size and frontend rendering work.
Exercise 4: Add Task Categories
Add a category field such as:
Work
Personal
Learning
Shopping
Then allow users to filter by category.
Exercise 5: Add Authentication
Protect the task endpoints with Spring Security.
Start with basic authentication in a local environment. Later, learn how to use token-based authentication.
Never store passwords as plain text. Passwords should be securely hashed.
Exercise 6: Write Tests
Write tests for:
- Creating a valid task.
- Rejecting an empty title.
- Fetching all tasks.
- Fetching a task by ID.
- Returning a not-found response.
- Toggling completion.
- Deleting a task.
Troubleshooting
PostgreSQL Connection Refused
Make sure PostgreSQL is running.
On Linux:
sudo systemctl status postgresql
Also check:
- The database name is
task_manager. - PostgreSQL is using port
5432. - The username is correct.
- The password is correct.
- The database server accepts local connections.
Password Authentication Failed
Test the database credentials directly:
psql -U postgres -h localhost -d task_manager
If this command fails, correct the PostgreSQL password or username before starting Spring Boot.
CORS Error
The React frontend runs on port 5173, while the backend runs on port 8080. The backend must allow requests from the frontend.
Check this annotation:
@CrossOrigin(origins = "http://localhost:5173")
If your frontend runs on a different port, update the origin.
For larger projects, configure CORS globally instead of adding the annotation to individual controllers.
404 Not Found
Check the request URL:
http://localhost:8080/api/tasks
Common mistakes include:
- Sending the request to port 5173 instead of 8080.
- Omitting
/api. - Using
/taskinstead of/tasks. - Forgetting to restart the application after changing a controller mapping.
React Shows an Empty List
Open the browser developer tools and inspect the Network tab.
Check that:
- The backend is running.
- The API returns JSON.
- The frontend calls port 8080.
- CORS allows port 5173.
- The database contains task records.
You can test the backend independently:
curl http://localhost:8080/api/tasks
Hibernate Does Not Create the Table
Check the following:
- The PostgreSQL driver dependency is present.
- The database exists.
- The entity has the
@Entityannotation. - The database credentials are correct.
- The application can connect to PostgreSQL.
-
spring.jpa.hibernate.ddl-auto=updateis present for local development.
For production applications, use database migration tools instead of depending on automatic schema updates.
Port Already in Use
If port 8080 is already occupied, change the backend port:
server.port=8081
Then update the React API URL:
const API_URL = "http://localhost:8081/api/tasks";
If port 5173 is occupied, Vite may automatically select another port. Update the backend CORS configuration accordingly.
Best Practices
Keep Layers Separate
Do not place database logic inside the controller.
A clean structure looks like this:
Controller → Service → Repository
This makes the code easier to understand and test.
Validate Input on the Backend
Frontend validation improves the user experience, but it cannot be trusted as the only protection. Users can call your API directly using tools such as curl or Postman.
Always validate important input on the backend.
Use DTOs in Larger Applications
This tutorial uses the Task entity directly in the request body to keep the project simple.
In a larger application, create request and response DTOs:
public class CreateTaskRequest {
private String title;
private String description;
}
DTOs help prevent internal database fields from being exposed accidentally.
Use Pagination
Do not return thousands of records in a single response. Pagination improves:
- Database performance.
- Network performance.
- Server memory usage.
- Frontend rendering speed.
Use Database Indexes Carefully
If you frequently filter by completion status, an index may help:
CREATE INDEX idx_tasks_completed
ON tasks(completed);
Indexes can speed up reads but add storage and write overhead. Add them based on actual query patterns.
Avoid Sensitive Data in Git
Never commit this type of configuration:
spring.datasource.password=real-password
Use environment variables or a secrets manager for sensitive information.
Use Database Migrations
The following setting is convenient during development:
spring.jpa.hibernate.ddl-auto=update
For production, consider a migration tool such as Flyway or Liquibase. Migrations make database changes explicit, reviewable, and repeatable.
Test Before Optimizing
Do not optimize based only on assumptions. Measure:
- API response time.
- Database query time.
- Frontend rendering time.
- Network payload size.
Then improve the actual bottleneck.
Performance Improvements
The first version of this project is suitable for learning, but you can improve its performance in several ways.
Reduce Unnecessary API Calls
After creating a task, the frontend adds the returned task directly to state:
setTasks((currentTasks) => [
...currentTasks,
newTask,
]);
This avoids fetching the entire task list again.
Use Database-Level Filtering
Do not fetch every task and filter them in Java if the database can perform the filtering:
List<Task> findByCompleted(boolean completed);
The database can process filtering more efficiently, especially as the table grows.
Add Pagination
Pagination prevents the server and browser from processing unnecessary records.
Disable SQL Logging in Production
This setting is useful while learning:
spring.jpa.show-sql=true
However, SQL logging can generate a large amount of output. Disable it in production unless you are actively debugging.
Use Connection Pooling
Spring Boot applications commonly use a database connection pool. For larger applications, tune pool settings based on traffic and database capacity rather than choosing large values blindly.
Learning Path
If you are building skills through a Java full-stack course in Bangalore with placement, use this project as a practical assignment rather than only reading the theory.
A useful learning path is:
- Learn Java classes, interfaces, collections, and exception handling.
- Practice SQL queries and relational database design.
- Understand HTTP methods, status codes, JSON, and REST.
- Learn Spring Boot dependency injection and configuration.
- Build REST APIs with Spring Web.
- Connect applications to databases using Spring Data JPA.
- Learn React components, state, forms, and effects.
- Use Git for version control.
- Write unit and integration tests.
- Add authentication with Spring Security.
- Containerize the application with Docker.
- Deploy the frontend, backend, and database separately.
Final Thoughts
This task manager gives you a complete Java full-stack project with a Spring Boot backend, React frontend, and PostgreSQL database.
You practiced:
- Designing REST endpoints.
- Creating a JPA entity.
- Connecting Spring Boot to PostgreSQL.
- Separating controller, service, and repository logic.
- Validating API input.
- Handling errors.
- Calling APIs from React.
- Managing frontend state.
- Testing endpoints with
curl. - Using Git to manage source code.
Do not stop after making the application work once. Add due dates, filtering, pagination, authentication, tests, Docker support, and deployment. Each improvement will help you understand how real full-stack applications are designed and maintained.
Top comments (0)