← Назад к вопросам

Реализация простого REST API с Spring Boot

1.8 Middle🔥 201 комментариев
#ORM и Hibernate#Spring Boot и Spring Data#Spring Framework

Условие

Создайте простое REST API для управления задачами (Todo List) с использованием Spring Boot.

Эндпоинты

  • GET /api/todos — получить все задачи
  • GET /api/todos/{id} — получить задачу по ID
  • POST /api/todos — создать новую задачу
  • PUT /api/todos/{id} — обновить задачу
  • DELETE /api/todos/{id} — удалить задачу

Модель данных

class Todo {
    Long id;
    String title;
    boolean completed;
}

Требования

  • Используйте @RestController
  • Возвращайте правильные HTTP статусы
  • Используйте DTO для запросов/ответов
  • Добавьте валидацию входных данных

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация простого REST API с Spring Boot

Это задача, которая проверяет владение Spring Boot и умение создавать производственный код с правильной обработкой ошибок, валидацией и правильными HTTP статусами.

Структура проекта

src/main/java/
├── com.example.todo/
│   ├── controller/
│   │   └── TodoController.java
│   ├── service/
│   │   └── TodoService.java
│   ├── repository/
│   │   └── TodoRepository.java
│   ├── entity/
│   │   └── Todo.java
│   ├── dto/
│   │   ├── CreateTodoRequest.java
│   │   ├── UpdateTodoRequest.java
│   │   └── TodoResponse.java
│   └── Application.java

1. Сущность Todo

package com.example.todo.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "todos")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String title;
    
    @Column(nullable = false)
    private boolean completed = false;
    
    @Column(name = "created_at")
    private Long createdAt = System.currentTimeMillis();
}

2. DTO классы

package com.example.todo.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateTodoRequest {
    @NotBlank(message = "Title cannot be empty")
    @Size(min = 1, max = 255, message = "Title must be between 1 and 255 characters")
    private String title;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateTodoRequest {
    @Size(min = 1, max = 255, message = "Title must be between 1 and 255 characters")
    private String title;
    
    private Boolean completed;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoResponse {
    private Long id;
    private String title;
    private boolean completed;
    private Long createdAt;
}

3. Repository

package com.example.todo.repository;

import com.example.todo.entity.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
}

4. Service (бизнес-логика)

package com.example.todo.service;

import com.example.todo.dto.CreateTodoRequest;
import com.example.todo.dto.TodoResponse;
import com.example.todo.dto.UpdateTodoRequest;
import com.example.todo.entity.Todo;
import com.example.todo.repository.TodoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class TodoService {
    
    private final TodoRepository todoRepository;
    
    /**
     * Получить все задачи
     */
    public List<TodoResponse> getAllTodos() {
        return todoRepository.findAll()
            .stream()
            .map(this::convertToResponse)
            .collect(Collectors.toList());
    }
    
    /**
     * Получить задачу по ID
     */
    public TodoResponse getTodoById(Long id) {
        Todo todo = todoRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
        return convertToResponse(todo);
    }
    
    /**
     * Создать новую задачу
     */
    public TodoResponse createTodo(CreateTodoRequest request) {
        Todo todo = new Todo();
        todo.setTitle(request.getTitle());
        todo.setCompleted(false);
        
        Todo saved = todoRepository.save(todo);
        return convertToResponse(saved);
    }
    
    /**
     * Обновить задачу
     */
    public TodoResponse updateTodo(Long id, UpdateTodoRequest request) {
        Todo todo = todoRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
        
        if (request.getTitle() != null && !request.getTitle().isBlank()) {
            todo.setTitle(request.getTitle());
        }
        
        if (request.getCompleted() != null) {
            todo.setCompleted(request.getCompleted());
        }
        
        Todo updated = todoRepository.save(todo);
        return convertToResponse(updated);
    }
    
    /**
     * Удалить задачу
     */
    public void deleteTodo(Long id) {
        if (!todoRepository.existsById(id)) {
            throw new ResourceNotFoundException("Todo not found with id: " + id);
        }
        todoRepository.deleteById(id);
    }
    
    /**
     * Конвертация сущности в DTO
     */
    private TodoResponse convertToResponse(Todo todo) {
        return new TodoResponse(
            todo.getId(),
            todo.getTitle(),
            todo.isCompleted(),
            todo.getCreatedAt()
        );
    }
}

5. Exception Handler

package com.example.todo.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "NOT_FOUND",
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(error);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult()
            .getAllErrors()
            .stream()
            .map(ObjectError::getDefaultMessage)
            .collect(Collectors.joining(", "));
        
        ErrorResponse error = new ErrorResponse(
            "VALIDATION_ERROR",
            message,
            System.currentTimeMillis()
        );
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(error);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_ERROR",
            "An unexpected error occurred",
            System.currentTimeMillis()
        );
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class ErrorResponse {
    private String code;
    private String message;
    private Long timestamp;
}

6. REST Controller

package com.example.todo.controller;

import com.example.todo.dto.CreateTodoRequest;
import com.example.todo.dto.TodoResponse;
import com.example.todo.dto.UpdateTodoRequest;
import com.example.todo.service.TodoService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/todos")
@RequiredArgsConstructor
public class TodoController {
    
    private final TodoService todoService;
    
    /**
     * GET /api/todos — получить все задачи
     */
    @GetMapping
    public ResponseEntity<List<TodoResponse>> getAllTodos() {
        List<TodoResponse> todos = todoService.getAllTodos();
        return ResponseEntity.ok(todos);
    }
    
    /**
     * GET /api/todos/{id} — получить задачу по ID
     */
    @GetMapping("/{id}")
    public ResponseEntity<TodoResponse> getTodoById(@PathVariable Long id) {
        TodoResponse todo = todoService.getTodoById(id);
        return ResponseEntity.ok(todo);
    }
    
    /**
     * POST /api/todos — создать новую задачу
     */
    @PostMapping
    public ResponseEntity<TodoResponse> createTodo(
            @Valid @RequestBody CreateTodoRequest request) {
        TodoResponse todo = todoService.createTodo(request);
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(todo);
    }
    
    /**
     * PUT /api/todos/{id} — обновить задачу
     */
    @PutMapping("/{id}")
    public ResponseEntity<TodoResponse> updateTodo(
            @PathVariable Long id,
            @Valid @RequestBody UpdateTodoRequest request) {
        TodoResponse todo = todoService.updateTodo(id, request);
        return ResponseEntity.ok(todo);
    }
    
    /**
     * DELETE /api/todos/{id} — удалить задачу
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTodo(@PathVariable Long id) {
        todoService.deleteTodo(id);
        return ResponseEntity.noContent().build();
    }
}

7. Application.java

package com.example.todo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

8. application.properties

spring.application.name=todo-api
server.port=8080

# Database
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA/Hibernate
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false

# Logging
logging.level.root=INFO
logging.level.com.example.todo=DEBUG

9. pom.xml (зависимости)

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

10. Тестирование с curl

# Получить все задачи (пусто)
curl http://localhost:8080/api/todos

# Создать задачу
curl -X POST http://localhost:8080/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Купить хлеб"}'

# Получить задачу по ID
curl http://localhost:8080/api/todos/1

# Обновить задачу
curl -X PUT http://localhost:8080/api/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Купить молоко","completed":true}'

# Удалить задачу
curl -X DELETE http://localhost:8080/api/todos/1

# Ошибка валидации (пустой title)
curl -X POST http://localhost:8080/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title":""}'

# Ошибка 404
curl http://localhost:8080/api/todos/999

Ключевые особенности решения

  1. @RestController — автоматически сериализует ответы в JSON
  2. @Valid — активирует валидацию на уровне контроллера
  3. HTTP статусы:
    • 200 OK — успешный GET/PUT
    • 201 CREATED — успешный POST
    • 204 NO CONTENT — успешный DELETE
    • 400 BAD REQUEST — ошибка валидации
    • 404 NOT FOUND — ресурс не найден
  4. DTO — отделяют внутреннее представление от внешнего API
  5. Service — содержит бизнес-логику
  6. Repository — только работа с БД
  7. Exception Handler — централизованная обработка ошибок

Это решение соответствует лучшим практикам Spring Boot и REST API дизайна.