ЛАБ 07

API Дизайн ба RESTful Үйлчилгээ

3.1 Лабораторийн зорилго

Энэ лабораторид та Spring Boot ашиглан RESTful API бүтээж, бодит дадлага хийнэ:

  1. Spring Boot төсөл үүсгэх
  2. CRUD REST API хөгжүүлэх (Entity, DTO, Repository, Service, Controller)
  3. Validation ба Exception Handling
  4. Swagger/OpenAPI баримт бичиг

Хэл: Java 17+ | Framework: Spring Boot 3.x | IDE: Eclipse IDE


3.2 Лаб 1: Spring Boot төсөл үүсгэх

Алхам 1: Spring Initializr ашиглах

https://start.spring.io руу орж:

ТохиргооУтга
ProjectMaven
LanguageJava
Spring Boot3.2.x
Groupcom.example
Artifactstudent-api
PackagingJar
Java17

Алхам 2: Dependency нэмэх

<!-- pom.xml -->
<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- JPA + Database -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- H2 Database (хөгжүүлэлтийн орчинд) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- Swagger/OpenAPI -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.3.0</version>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Алхам 3: application.yml тохиргоо

# src/main/resources/application.yml
server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:mem:studentdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true

springdoc:
  swagger-ui:
    path: /swagger-ui.html

Алхам 4: Ажиллуулах

./mvnw spring-boot:run

Шалгах:

  • http://localhost:8080/h2-console → H2 Database console
  • http://localhost:8080/swagger-ui.html → Swagger UI

3.3 Лаб 2: CRUD REST API хөгжүүлэх

Алхам 1: Entity үүсгэх

// src/main/java/com/example/studentapi/entity/Student.java
package com.example.studentapi.entity;

import jakarta.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private Double gpa;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private StudentStatus status = StudentStatus.ACTIVE;

    @Column(name = "enrolled_at")
    private LocalDate enrolledAt = LocalDate.now();

    public Student() {}

    public Student(String name, String email, Double gpa) {
        this.name = name;
        this.email = email;
        this.gpa = gpa;
    }

    // Getter, Setter методууд
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Double getGpa() { return gpa; }
    public void setGpa(Double gpa) { this.gpa = gpa; }
    public StudentStatus getStatus() { return status; }
    public void setStatus(StudentStatus status) { this.status = status; }
    public LocalDate getEnrolledAt() { return enrolledAt; }
    public void setEnrolledAt(LocalDate enrolledAt) { this.enrolledAt = enrolledAt; }
}
// src/main/java/com/example/studentapi/entity/StudentStatus.java
package com.example.studentapi.entity;

public enum StudentStatus {
    ACTIVE, INACTIVE, GRADUATED, SUSPENDED
}

Алхам 2: DTO үүсгэх

// src/main/java/com/example/studentapi/dto/StudentCreateRequest.java
package com.example.studentapi.dto;

import jakarta.validation.constraints.*;

public record StudentCreateRequest(
    @NotBlank(message = "Нэр хоосон байж болохгүй")
    @Size(min = 2, max = 100, message = "Нэр 2-100 тэмдэгт байх ёстой")
    String name,

    @NotBlank(message = "Имэйл хоосон байж болохгүй")
    @Email(message = "Имэйл формат буруу")
    String email,

    @NotNull(message = "GPA заавал оруулна")
    @Min(value = 0, message = "GPA 0-аас бага байж болохгүй")
    @Max(value = 4, message = "GPA 4-аас их байж болохгүй")
    Double gpa
) {}
// src/main/java/com/example/studentapi/dto/StudentResponse.java
package com.example.studentapi.dto;

import com.example.studentapi.entity.Student;
import java.time.LocalDate;

public record StudentResponse(
    Long id,
    String name,
    String email,
    Double gpa,
    String status,
    LocalDate enrolledAt
) {
    public static StudentResponse from(Student student) {
        return new StudentResponse(
            student.getId(),
            student.getName(),
            student.getEmail(),
            student.getGpa(),
            student.getStatus().name(),
            student.getEnrolledAt()
        );
    }
}

Алхам 3: Repository үүсгэх

// src/main/java/com/example/studentapi/repository/StudentRepository.java
package com.example.studentapi.repository;

import com.example.studentapi.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface StudentRepository extends JpaRepository<Student, Long> {
    Optional<Student> findByEmail(String email);
    boolean existsByEmail(String email);
}

Алхам 4: Custom Exception үүсгэх

// src/main/java/com/example/studentapi/exception/StudentNotFoundException.java
package com.example.studentapi.exception;

public class StudentNotFoundException extends RuntimeException {
    public StudentNotFoundException(String message) {
        super(message);
    }
}
// src/main/java/com/example/studentapi/exception/DuplicateEmailException.java
package com.example.studentapi.exception;

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

Алхам 5: Service үүсгэх

// src/main/java/com/example/studentapi/service/StudentService.java
package com.example.studentapi.service;

import com.example.studentapi.dto.*;
import com.example.studentapi.entity.Student;
import com.example.studentapi.exception.*;
import com.example.studentapi.repository.StudentRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class StudentService {

    private final StudentRepository repository;

    public StudentService(StudentRepository repository) {
        this.repository = repository;
    }

    public List<StudentResponse> findAll() {
        return repository.findAll().stream()
            .map(StudentResponse::from)
            .toList();
    }

    public StudentResponse findById(Long id) {
        Student student = repository.findById(id)
            .orElseThrow(() -> new StudentNotFoundException(
                "ID=" + id + " оюутан олдсонгүй"
            ));
        return StudentResponse.from(student);
    }

    @Transactional
    public StudentResponse create(StudentCreateRequest request) {
        if (repository.existsByEmail(request.email())) {
            throw new DuplicateEmailException(
                "'" + request.email() + "' имэйл бүртгэлтэй байна"
            );
        }
        Student student = new Student(
            request.name(), request.email(), request.gpa()
        );
        Student saved = repository.save(student);
        return StudentResponse.from(saved);
    }

    @Transactional
    public StudentResponse update(Long id, StudentCreateRequest request) {
        Student student = repository.findById(id)
            .orElseThrow(() -> new StudentNotFoundException(
                "ID=" + id + " оюутан олдсонгүй"
            ));
        student.setName(request.name());
        student.setEmail(request.email());
        student.setGpa(request.gpa());
        Student saved = repository.save(student);
        return StudentResponse.from(saved);
    }

    @Transactional
    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new StudentNotFoundException(
                "ID=" + id + " оюутан олдсонгүй"
            );
        }
        repository.deleteById(id);
    }
}

Алхам 6: Controller үүсгэх

// src/main/java/com/example/studentapi/controller/StudentController.java
package com.example.studentapi.controller;

import com.example.studentapi.dto.*;
import com.example.studentapi.service.StudentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/api/v1/students")
@Tag(name = "Student API", description = "Оюутны CRUD API")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @GetMapping
    @Operation(summary = "Бүх оюутан авах")
    public ResponseEntity<List<StudentResponse>> findAll() {
        return ResponseEntity.ok(studentService.findAll());
    }

    @GetMapping("/{id}")
    @Operation(summary = "ID-аар оюутан авах")
    public ResponseEntity<StudentResponse> findById(@PathVariable Long id) {
        return ResponseEntity.ok(studentService.findById(id));
    }

    @PostMapping
    @Operation(summary = "Шинэ оюутан үүсгэх")
    public ResponseEntity<StudentResponse> create(
            @Valid @RequestBody StudentCreateRequest request) {
        StudentResponse created = studentService.create(request);
        URI location = URI.create("/api/v1/students/" + created.id());
        return ResponseEntity.created(location).body(created);
    }

    @PutMapping("/{id}")
    @Operation(summary = "Оюутны мэдээлэл шинэчлэх")
    public ResponseEntity<StudentResponse> update(
            @PathVariable Long id,
            @Valid @RequestBody StudentCreateRequest request) {
        return ResponseEntity.ok(studentService.update(id, request));
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "Оюутан устгах")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        studentService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Алхам 7: Global Exception Handler

// src/main/java/com/example/studentapi/exception/GlobalExceptionHandler.java
package com.example.studentapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(StudentNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(StudentNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(), "Not Found",
            ex.getMessage(), LocalDateTime.now(), null
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(DuplicateEmailException.class)
    public ResponseEntity<ErrorResponse> handleDuplicate(DuplicateEmailException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.CONFLICT.value(), "Conflict",
            ex.getMessage(), LocalDateTime.now(), null
        );
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex) {
        List<String> details = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(), "Validation Error",
            "Оролтын өгөгдөл буруу", LocalDateTime.now(), details
        );
        return ResponseEntity.badRequest().body(error);
    }
}
// src/main/java/com/example/studentapi/exception/ErrorResponse.java
package com.example.studentapi.exception;

import java.time.LocalDateTime;
import java.util.List;

public record ErrorResponse(
    int status,
    String error,
    String message,
    LocalDateTime timestamp,
    List<String> details
) {}

3.4 Лаб 3: API тестлэх (cURL / Swagger)

cURL ашиглан тестлэх:

# 1. Шинэ оюутан үүсгэх (POST)
curl -X POST http://localhost:8080/api/v1/students \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Батболд",
    "email": "batbold@example.com",
    "gpa": 3.75
  }'
# Хариу: 201 Created + StudentResponse JSON

# 2. Бүх оюутан авах (GET)
curl http://localhost:8080/api/v1/students
# Хариу: 200 OK + List<StudentResponse>

# 3. ID-аар оюутан авах (GET)
curl http://localhost:8080/api/v1/students/1
# Хариу: 200 OK + StudentResponse

# 4. Оюутны мэдээлэл шинэчлэх (PUT)
curl -X PUT http://localhost:8080/api/v1/students/1 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Батболд Дорж",
    "email": "batbold.dorj@example.com",
    "gpa": 3.80
  }'
# Хариу: 200 OK + Шинэчлэгдсэн StudentResponse

# 5. Оюутан устгах (DELETE)
curl -X DELETE http://localhost:8080/api/v1/students/1
# Хариу: 204 No Content

# 6. Validation алдаа тестлэх
curl -X POST http://localhost:8080/api/v1/students \
  -H "Content-Type: application/json" \
  -d '{
    "name": "",
    "email": "буруу-имэйл",
    "gpa": 5.0
  }'
# Хариу: 400 Bad Request + Validation алдааны жагсаалт

# 7. Олдохгүй оюутан тестлэх
curl http://localhost:8080/api/v1/students/999
# Хариу: 404 Not Found + "ID=999 оюутан олдсонгүй"

Swagger UI ашиглан тестлэх:

  1. http://localhost:8080/swagger-ui.html нээх
  2. "Student API" бүлгийг дарж нээх
  3. "Try it out" товч дарах
  4. Параметр оруулж "Execute" дарах
  5. Response шалгах

3.5 Лаб 4: Хуудаслалт ба Шүүлт нэмэх

Controller-д хуудаслалт нэмэх:

// StudentController.java — findAll методыг шинэчлэх
@GetMapping
@Operation(summary = "Оюутны жагсаалт (хуудаслалттай)")
public ResponseEntity<Page<StudentResponse>> findAll(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "id") String sortBy,
        @RequestParam(defaultValue = "asc") String order) {

    Sort sort = order.equalsIgnoreCase("desc")
        ? Sort.by(sortBy).descending()
        : Sort.by(sortBy).ascending();

    Page<StudentResponse> result = studentService.findAll(
        PageRequest.of(page, size, sort)
    );
    return ResponseEntity.ok(result);
}
// StudentService.java — findAll методыг шинэчлэх
public Page<StudentResponse> findAll(Pageable pageable) {
    return repository.findAll(pageable)
        .map(StudentResponse::from);
}

Тестлэх:

# 1-р хуудас, 5 бичлэг, GPA-аар буурах
curl "http://localhost:8080/api/v1/students?page=0&size=5&sortBy=gpa&order=desc"