#java /

Getting Started with Spring Boot: Building Your First RESTful API

Build a complete RESTful API from scratch with Spring Boot, covering project configuration, CRUD operations, data validation, and exception handling.

Goal

Build a production-grade Spring Boot RESTful API service from scratch and master the core concepts of backend development.

Background

Why Should Frontend Engineers Learn Backend?

In the trend of full-stack development, it's becoming increasingly important for frontend engineers to understand backend API design and implementation. Spring Boot is the most popular backend framework in the Java ecosystem. Mastering it allows you to:

  1. Complete full-stack projects independently
  2. Better understand frontend-backend interaction
  3. Collaborate more efficiently with backend teams

Project Initialization

Using Spring Initializr

Visit start.spring.io to create a project:

  • Project: Maven
  • Language: Java
  • Spring Boot: 2.5.x
  • Packaging: Jar
  • Java: 11+
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver, Lombok

Project Structure

demo-api/
├── src/
│   ├── main/
│   │   ├── java/com/example/demo/
│   │   │   ├── DemoApplication.java
│   │   │   ├── controller/
│   │   │   │   └── UserController.java
│   │   │   ├── model/
│   │   │   │   └── User.java
│   │   │   ├── repository/
│   │   │   │   └── UserRepository.java
│   │   │   ├── service/
│   │   │   │   └── UserService.java
│   │   │   └── exception/
│   │   │       └── ResourceNotFoundException.java
│   │   └── resources/
│   │       ├── application.yml
│   │       └── schema.sql
│   └── test/
├── pom.xml
└── README.md

Core Code Implementation

1. Entity Class (Model)

package com.example.demo.model;
import lombok.Data;
import javax.persistence.*;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Username cannot be blank")
@Size(min = 2, max = 50, message = "Username length must be between 2-50")
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "Email cannot be blank")
@Email(message = "Email format is incorrect")
@Column(unique = true, nullable = false)
private String email;
@Size(max = 200, message = "Nickname cannot exceed 200 characters")
private String nickname;
@Column(nullable = false)
private Boolean enabled = true;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}

2. Data Access Layer (Repository)

package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
// Custom query
@Query("SELECT u FROM User u WHERE u.enabled = true")
List<User> findAllActiveUsers();
}

3. Business Logic Layer (Service)

package com.example.demo.service;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// Get all users (paginated)
public Page<User> getAllUsers(int page, int size, String sortBy) {
Sort sort = Sort.by(sortBy).descending();
return userRepository.findAll(PageRequest.of(page, size, sort));
}
// Get user by ID
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found, ID: " + id));
}
// Create user
public User createUser(User user) {
if (userRepository.existsByUsername(user.getUsername())) {
throw new RuntimeException("Username already exists: " + user.getUsername());
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("Email already in use: " + user.getEmail());
}
return userRepository.save(user);
}
// Update user
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
user.setUsername(userDetails.getUsername());
user.setEmail(userDetails.getEmail());
user.setNickname(userDetails.getNickname());
user.setEnabled(userDetails.getEnabled());
return userRepository.save(user);
}
// Delete user
public void deleteUser(Long id) {
User user = getUserById(id);
userRepository.delete(user);
}
// Search users
public List<User> searchUsers(String keyword) {
return userRepository.findByUsernameContainingOrEmailContaining(keyword, keyword);
}
}

4. Controller Layer

package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
@Autowired
private UserService userService;
// Get all users
@GetMapping
public ResponseEntity<Page<User>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sortBy) {
return ResponseEntity.ok(userService.getAllUsers(page, size, sortBy));
}
// Get user by ID
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
// Create user
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User createdUser = userService.createUser(user);
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
// Update user
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Valid @RequestBody User userDetails) {
return ResponseEntity.ok(userService.updateUser(id, userDetails));
}
// Delete user
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Boolean>> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
Map<String, Boolean> response = new HashMap<>();
response.put("deleted", true);
return ResponseEntity.ok(response);
}
// Search users
@GetMapping("/search")
public ResponseEntity<List<User>> searchUsers(@RequestParam String keyword) {
return ResponseEntity.ok(userService.searchUsers(keyword));
}
}

5. Global Exception Handling

package com.example.demo.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(ResourceNotFoundException.class)
public ResponseEntity<Map<String, String>> handleResourceNotFound(ResourceNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

6. Configuration File

# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: UTC

API Testing

Testing with curl

# Create user
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"username": "zhangsan",
"email": "zhangsan@example.com",
"nickname": "Zhang San"
}'
# Get all users
curl http://localhost:8080/api/users
# Get user by ID
curl http://localhost:8080/api/users/1
# Update user
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{
"username": "zhangsan",
"email": "zhangsan@example.com",
"nickname": "Zhang Sanfeng"
}'
# Delete user
curl -X DELETE http://localhost:8080/api/users/1

Best Practices

1. Unified Response Format

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "Operation successful", data);
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}

2. DTO Separation

// Request DTO
@Data
public class CreateUserRequest {
@NotBlank(message = "Username cannot be blank")
private String username;
@NotBlank(message = "Email cannot be blank")
@Email(message = "Email format is incorrect")
private String email;
}
// Response DTO
@Data
public class UserResponse {
private Long id;
private String username;
private String email;
private String nickname;
private LocalDateTime createdAt;
}

Summary

  1. Spring Boot simplifies Java backend development: Auto-configuration, convention over configuration
  2. Layered architecture: Controller → Service → Repository, clear responsibilities
  3. Data validation: Declarative validation using annotations
  4. Exception handling: Unified handling with global exception handlers
  5. RESTful design: Standard HTTP methods and status codes

Master these, and you can quickly build a production-grade backend API service.

Like this post? Tweet to share it with others or open an issue to discuss with me!