Эксперимент: Todo API на Spring Boot - часть 1

Эксперимент: Todo API на Spring Boot - часть 1

Цель

  1. В дальнейшем мы с вами рассмотрим, примеры Todo Rest API, которое будет написано на различных технологиях, как инструментах хранения данных, так и framework-ах, еще поиграем с транспортом.‌
    ‌Собственно говоря, целью этого эксперимента является возможность расширить как мой, так и ваш горизонт стека технологий, считаем что прелюдия закончена и переходим к действиям;
  2. В этой статье будем использовать Java 8, DB - H2 in-memory для простоты работы, Spring Boot, Spring Data JPA, JUnit;
  3. Итак со стеком технологий определились, в чем же суть реализации, какие контракты мы будем поддерживать, и как будем с этим работать. Установим за правило описать REST контракты перед тем как их реализовывать. Итак:‌
    ‌ - GET - /api/todo - получаем список объектов‌
    ‌ - POST - /api/todo - сохраняем объект‌
    ‌ - PUT - /api/todo - обновляем объект‌
    ‌ - DELETE - /api/todo/{id} - здесь мы удаляем объект по его id - который отмечен как переменная ресурса ‌
    ‌ Следовательно можем приступить к разработке.

Первые шаги

Для создания проекта на Spring Boot и его зависимостей воспользуемся ссылкой start.spring.io

Меняем артефакты под себя и добавляем зависимости

Для нашего первого примера оставляем левую часть п умолчанию, единственное редактируем название package-а и имя артефакта, ‌
‌далее добавляем:‌

  • Spring Web
  • Spring Data JPA
  • Lombok(optional)
  • H2 Database

А еще есть вот такой shortcut как сразу этот список зависимостей открыть 👇‌
‌Вот ссылка
‌Нажимаем GENERATE и скачиваем zip архив с проектом

Есть проекта идея, идем в IntelliJ IDEA!

Открываем в IDEA наш разархивированный проект, дожидаемся пока откомпилируется и приступаем к написанию кода.‌

Запускаем проект, чтобы убедиться, что он по умолчанию работает ;)

Убедившись, что проект работает на порту 8080 создаем пакеты, для структурирования нашего кода. В дальнейшем, я буду придерживаться своей структуры в ваших проектах, структура может отличаться это больше зависит от бэкграунда вашего и коллег, архитектора, тимлида и др. субъектов которые могут повлиять на ваш код. Так вот в сторону эти отступы, и обратно к структуре:‌

‌‌

  • configs - Конфигурации приложения
  • controllers - Контроллеры отвечающие за REST API
  • services - здесь описываем бизнес логику
  • domain - доменная модель данных
  • repositories - слой работы с базой данных
package com.timurisachenko.todo.domain;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
@Data
@NoArgsConstructor
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private boolean done;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

Затем идем в директорию repositories  и создаем интерфейс TodoRepository, который наследуется от JpaRepository<Todo, Long> 👇‌

package com.timurisachenko.todo.repositories;

import com.timurisachenko.todo.domain.Todo;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TodoRepository extends JpaRepository<Todo, Long> {
}
Todo Domain Model

Создаем класс TodoService отвечающий за бизнес логику в пакете services

package com.timurisachenko.todo.services;

import com.timurisachenko.todo.domain.Todo;
import com.timurisachenko.todo.repositories.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TodoService {
    private final TodoRepository todoRepository;

    @Autowired
    public TodoService(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    public List<Todo> getAll() {
        return todoRepository.findAll();
    }

    public Todo create(Todo todo) {
        return todoRepository.save(todo);
    }

    public Todo update(Todo todo) {
        return todoRepository.save(todo);
    }

    public void delete(Long id) {
        todoRepository.deleteById(id);
    }
}

Отлично, сервис создан и остается написать презентационный слой у нас это будет TodoController 👌 ‌

package com.timurisachenko.todo.controllers;

import com.timurisachenko.todo.domain.Todo;
import com.timurisachenko.todo.services.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/todo")
public class TodoController {
    private final TodoService todoService;

    @Autowired
    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @GetMapping
    public List<Todo> getAll() {
        return todoService.getAll();
    }

    @PostMapping
    public Todo create(@RequestBody Todo todo) {
        return todoService.create(todo);
    }

    @PutMapping
    public Todo update(@RequestBody Todo todo) {
        return todoService.update(todo);
    }
    
    @DeleteMapping(value = "/{id}")
    public void deleteById(@PathVariable("id") Long id) {
        todoService.delete(id);
    }
}

Всё сервис готов, можно запускать! Переходим по ссылке http://localhost:8080/api/todo

И ВИДИМ 👇🏿

[]

отлично сервис отработал, так как мы использовали spring data jpa, то и таблицу создавать не пришлось, springboot автоматически создал БД h2 в памяти и создал в ней таблицу todo с нашими полями.‌
‌Можем вызвать все Rest запросы, что мы с вами создали 👆в контроллере‌
‌Все отработает, но при старте данных нету. Именно поэтому предлагаю предустановить данные, а сделаем мы это в корневом классе приложения TodoApplication  ->‌

package com.timurisachenko.todo;

import com.timurisachenko.todo.domain.Todo;
import com.timurisachenko.todo.repositories.TodoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class TodoApplication {

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

    private static final Logger log = LoggerFactory.getLogger(TodoApplication.class);
    @Bean
    public CommandLineRunner demo(TodoRepository repository) {
        return (args) -> {
            // save a few todo which already have done
            repository.save(new Todo("Помыть посуду", true));
            repository.save(new Todo("Пробежка 5 км", true));
            repository.save(new Todo("Прочитать книгу", true));
            repository.save(new Todo("Посмотреть ютуб новый выпуск !Что было дальше", true));
            repository.save(new Todo("Заказать новый циркуль", true));

            // fetch all Todo
            log.info("Todo found with findAll():");
            log.info("-------------------------------");
            for (Todo todo : repository.findAll()) {
                log.info(todo.toString());
            }
            log.info("");

        };
    }
}

Теперь при рестарте мы получаем список Todo элементов в json формате‌
‌‌

[
	{
		id: 1,
		title: "Помыть посуду",
		done: true
	},
	{
		id: 2,
		title: "Пробежка 5 км",
		done: true
	},
	{
		id: 3,
		title: "Прочитать книгу",
		done: true
	},
	{
		id: 4,
		title: "Посмотреть ютуб новый выпуск !Что было дальше",
		done: true
	},
	{
		id: 5,
		title: "Заказать новый циркуль",
		done: true
	}
]

Отлично! Все готово, всё работает только с одним НО ‌
‌которое я покажу вам в видео по работе этого приложения.‌
‌‌
‌И еще момент в консоли можно делать запрос по аналогии с curl -ом‌
‌сохраню примеры, вдруг вам пригодится:‌
‌‌

//GET
await (await fetch("/api/todo", {
  method: "get",
  headers: {
    'Accept': 'application/json'
   }
})).json()
//POST
await (await fetch("/api/todo", {
  method: "post",
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },

  //make sure to serialize your JSON body
  body: JSON.stringify({
    title: "Todo1",
    done: false
  })
})).json()

//PUT
await (await fetch("/api/todo", {
  method: "put",
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },

  //make sure to serialize your JSON body
  body: JSON.stringify({
    id: 1, 
    title: "Blablahblah",
    done: true
  })
})).json()
//DELETE
await (await fetch("/api/todo/1", {
  method: "delete",
  headers: {
    'Accept': 'application/json'
   }
})).json()

Всем спасибо! Хорошего дня! Пишите, если вам понравилось и хотелось бы, чтобы появилось видео руководство.