# Lab 6 Walkthrough — Spring Boot REST API: Students CRUD

> **Audience:** Lecturer guiding a 3-hour lab.
> **How to use:** Read top to bottom. Every code block is meant to be **copied verbatim** into the editor or terminal. Each "STEP" is one paste-and-explain moment.
> **End state:** A working Spring Boot REST API that creates / reads / updates / deletes students, validates input, returns clean error JSON, and ships with Swagger UI.

The full finished project lives at [examples/lab06_students_api/](../examples/lab06_students_api/) — open it in IntelliJ or VS Code if you'd rather demo from a complete copy.

---

## Table of contents

1. [What we are building (3 min)](#1-what-we-are-building-3-min)
2. [Prerequisites & sanity check (5 min)](#2-prerequisites--sanity-check-5-min)
3. [Step-by-step build (90–110 min)](#3-step-by-step-build)
   - STEP 1 — Generate the project from Spring Initializr
   - STEP 2 — Start a local Postgres with Docker
   - STEP 3 — Open the project and run the empty app
   - STEP 4 — Configure `application.properties`
   - STEP 5 — Create package structure
   - STEP 6 — Write the `Student` entity
   - STEP 7 — Write the `StudentRepository`
   - STEP 8 — Write the three DTOs
   - STEP 9 — Write custom exceptions
   - STEP 10 — Write the `StudentService`
   - STEP 11 — Write the `StudentController`
   - STEP 12 — Write the `GlobalExceptionHandler`
   - STEP 13 — Seed some data on startup
   - STEP 14 — First run + first request
4. [Test every endpoint (20 min)](#4-test-every-endpoint)
5. [Swagger UI (5 min)](#5-swagger-ui)
6. [What students must submit](#6-what-students-must-submit)
7. [Common errors and fixes](#7-common-errors-and-fixes)

---

## 1. What we are building (3 min)

A REST API that manages student records. By the end of the lab the API will support:

| Method   | URL                       | Purpose                  |
|----------|---------------------------|--------------------------|
| `GET`    | `/api/students`           | List all students        |
| `GET`    | `/api/students?course=BCS`| Filter by course         |
| `GET`    | `/api/students/{id}`      | Get one student          |
| `POST`   | `/api/students`           | Create a student         |
| `PUT`    | `/api/students/{id}`      | Replace a student        |
| `DELETE` | `/api/students/{id}`      | Delete a student         |

### The architecture in one diagram

```
HTTP request
     │
     ▼
┌─────────────────────┐
│ StudentController   │  ← @RestController (thin: HTTP only)
└─────────────────────┘
     │
     ▼
┌─────────────────────┐
│ StudentService      │  ← @Service (business rules + transactions)
└─────────────────────┘
     │
     ▼
┌─────────────────────┐
│ StudentRepository   │  ← extends JpaRepository (CRUD comes free)
└─────────────────────┘
     │
     ▼
   H2 in-memory DB     ← students table
```

Plus a side-channel: `GlobalExceptionHandler` (`@RestControllerAdvice`) catches every exception thrown anywhere above and turns it into clean JSON.

### Why these layers

- **Controller** — speaks HTTP. Translates JSON to objects and back. **Zero** business logic.
- **Service** — knows the rules ("email must be unique", "year must be 1–4 enforced beyond validation"). All `@Transactional` work happens here.
- **Repository** — knows the database. We use Spring Data JPA so the basic CRUD methods are generated for us.
- **DTOs** — the public shape of our API. We **never** return the `@Entity` directly to clients; that leaks DB columns and opens us to over-posting attacks.

---

## 2. Prerequisites & sanity check (5 min)

Run these four commands. All four must work before continuing.

```bash
java -version           # expect 21 (any modern JDK ≥ 17 works, but Initializr will assume 21)
mvn -version            # expect Apache Maven ≥ 3.9
docker --version        # we use Docker to run Postgres locally
curl --version          # any version
```

If any of those fail, fix it before proceeding — see the [Setup Guide](../resources/setup_guide.md).

---

## 3. Step-by-step build

### STEP 1 — Generate the project from Spring Initializr

Open https://start.spring.io and fill it in:

| Field         | Value                              |
|---------------|------------------------------------|
| Project       | **Maven**                          |
| Language      | **Java**                           |
| Spring Boot   | **3.3.4** (or latest 3.3.x)        |
| Group         | `tz.ac.suza.wt`                    |
| Artifact      | `students-api`                     |
| Packaging     | Jar                                |
| Java          | **21**                             |

Click **ADD DEPENDENCIES** and tick **all five**:

- **Spring Web**
- **Spring Data JPA**
- **Validation**
- **PostgreSQL Driver**
- **Lombok**

Click **GENERATE**. A `students-api.zip` downloads.

```bash
unzip students-api.zip -d ~/Code/
cd ~/Code/students-api
```

> 💡 **Teach moment.** Initializr is just a UI over a Maven archetype. You can do the same thing with a single `curl` command — but the website is friendlier for class.

### STEP 2 — Start a local Postgres with Docker

Spring needs a real database to talk to. We use **PostgreSQL** — the same database students will use in production. Docker means "no install ceremony": one command and we have a running DB.

Create `docker-compose.yml` in the project root:

```yaml
services:
  db:
    image: postgres:16-alpine
    container_name: students-api-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: studentsdb
      POSTGRES_USER: studentsapi
      POSTGRES_PASSWORD: studentsapi
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U studentsapi -d studentsdb"]
      interval: 5s
      retries: 10

volumes:
  pgdata:
```

Start it:

```bash
docker compose up -d
docker compose ps        # status column should say "healthy" after ~5s
```

You now have a Postgres running at `localhost:5432`, with:

- Database name: `studentsdb`
- User: `studentsapi`
- Password: `studentsapi`

> 💡 **Teach moment.** The named volume `pgdata` keeps the data alive between
> restarts. `docker compose down -v` (with `-v`) deletes the volume too —
> handy for a clean slate, dangerous in production.

You can connect from the host with `psql` or DBeaver:

```bash
docker exec -it students-api-db psql -U studentsapi -d studentsdb
# inside psql:
\l        # list databases
\dt       # list tables (none yet)
\q
```

### STEP 3 — Open the project in your editor

Open the folder in IntelliJ IDEA or VS Code. Wait for it to import the Maven project — that downloads Spring Boot and all its transitive dependencies (200 MB of jars, one-time).

> 💡 **Teach moment.** We can't run the app yet — Spring needs a datasource URL and we haven't given it one. We'll do that in STEP 4 and start the app in STEP 14.

### STEP 4 — Configure `application.properties`

Replace the contents of `src/main/resources/application.properties` with:

```properties
spring.application.name=students-api

# ----- PostgreSQL ---------------------------------------------------------
# Override these via env vars in production:
#   SPRING_DATASOURCE_URL=jdbc:postgresql://prod-host:5432/studentsdb
#   SPRING_DATASOURCE_USERNAME=...   SPRING_DATASOURCE_PASSWORD=...
spring.datasource.url=jdbc:postgresql://localhost:5432/studentsdb
spring.datasource.username=studentsapi
spring.datasource.password=studentsapi
spring.datasource.driver-class-name=org.postgresql.Driver

# ----- JPA / Hibernate ----------------------------------------------------
# 'update' is fine for development; for production use Flyway/Liquibase migrations
# and set ddl-auto=validate.
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.open-in-view=false

# ----- Swagger / OpenAPI --------------------------------------------------
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.api-docs.path=/v3/api-docs
```

> 💡 **Teach moment.**
> - `ddl-auto=update` lets Hibernate add columns when entities change. **Never** use this in production — write proper migrations with Flyway or Liquibase.
> - `spring.jpa.open-in-view=false` turns off a feature that's convenient for templated MVC apps but harmful for REST APIs (it keeps DB connections open way too long).
> - Every property here is overridable by env var. `SPRING_DATASOURCE_PASSWORD` is the standard name Spring reads in production.

### STEP 5 — Create package structure

Inside `src/main/java/tz/ac/suza/wt/studentsapi/` create six new packages:

```
config/
controller/
dto/
entity/
exception/
repository/
service/
```

> 💡 **Teach moment.** We're organising **by layer**, not by feature. Both are valid. By-layer is easier to learn; by-feature scales better. We pick by-layer here.

### STEP 6 — Write the `Student` entity

Create `entity/Student.java`:

```java
package tz.ac.suza.wt.studentsapi.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.Instant;

@Entity
@Table(name = "students")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student {

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

    @Column(nullable = false, length = 100)
    private String name;

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

    @Column(nullable = false, length = 100)
    private String course;

    @Column(nullable = false)
    private Integer year;

    @Column(nullable = false, updatable = false)
    private Instant createdAt;
}
```

> 💡 **Teach moment.**
> - `@Entity` + `@Table` tells JPA "this class maps to the `students` table".
> - Lombok's `@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor` removes 80 lines of getters, setters, and constructors. **Make sure** students have the IDE's Lombok plugin enabled, or the code won't compile in the editor.
> - **No validation annotations here.** Validation belongs on the DTO. The entity defines the storage shape; the DTO defines the public input shape. We will keep them separate.

### STEP 7 — Write the `StudentRepository`

Create `repository/StudentRepository.java`:

```java
package tz.ac.suza.wt.studentsapi.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tz.ac.suza.wt.studentsapi.entity.Student;

import java.util.List;
import java.util.Optional;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    List<Student> findByCourse(String course);

    Optional<Student> findByEmail(String email);

    boolean existsByEmail(String email);
}
```

> 💡 **Teach moment.** We didn't write any SQL. Spring Data JPA reads our method names and **generates the queries at runtime**: `findByCourse` becomes `SELECT * FROM students WHERE course = ?`. This is called **derived query methods**. It's magical the first time you see it.

### STEP 8 — Write the three DTOs

We need three:

- `StudentCreateDto` — incoming JSON for `POST`
- `StudentUpdateDto` — incoming JSON for `PUT`
- `StudentResponseDto` — outgoing JSON to clients

#### `dto/StudentCreateDto.java`

```java
package tz.ac.suza.wt.studentsapi.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record StudentCreateDto(

        @NotBlank(message = "name is required")
        @Size(min = 2, max = 100, message = "name must be 2..100 characters")
        String name,

        @NotBlank(message = "email is required")
        @Email(message = "email must be a valid address")
        @Size(max = 150)
        String email,

        @NotBlank(message = "course is required")
        @Size(min = 2, max = 100)
        String course,

        @NotNull(message = "year is required")
        @Min(value = 1, message = "year must be between 1 and 4")
        @Max(value = 4, message = "year must be between 1 and 4")
        Integer year
) {}
```

#### `dto/StudentUpdateDto.java`

```java
package tz.ac.suza.wt.studentsapi.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record StudentUpdateDto(

        @NotBlank @Size(min = 2, max = 100)
        String name,

        @NotBlank @Email @Size(max = 150)
        String email,

        @NotBlank @Size(min = 2, max = 100)
        String course,

        @NotNull @Min(1) @Max(4)
        Integer year
) {}
```

#### `dto/StudentResponseDto.java`

```java
package tz.ac.suza.wt.studentsapi.dto;

import tz.ac.suza.wt.studentsapi.entity.Student;

import java.time.Instant;

public record StudentResponseDto(
        Long id,
        String name,
        String email,
        String course,
        Integer year,
        Instant createdAt
) {
    public static StudentResponseDto from(Student s) {
        return new StudentResponseDto(
                s.getId(), s.getName(), s.getEmail(),
                s.getCourse(), s.getYear(), s.getCreatedAt()
        );
    }
}
```

> 💡 **Teach moment.**
> - We use Java **records** — concise, immutable, perfect for DTOs.
> - The validation annotations (`@NotBlank`, `@Email`, `@Min`, `@Max`) belong on the incoming DTOs, not on the entity. They protect us *at the API boundary*.
> - `StudentResponseDto.from(...)` is a tiny static mapper. In a bigger project, use **MapStruct** to generate these.
> - **Why separate Create and Update DTOs?** Today they look identical. Tomorrow you'll want different rules — e.g. `email` immutable on update, or `password` required only on create. Splitting them now costs nothing and saves pain later.

### STEP 9 — Write custom exceptions

These let the service throw meaningful errors that the advice can catch later.

#### `exception/StudentNotFoundException.java`

```java
package tz.ac.suza.wt.studentsapi.exception;

public class StudentNotFoundException extends RuntimeException {
    public StudentNotFoundException(Long id) {
        super("Student with id " + id + " was not found");
    }
}
```

#### `exception/EmailAlreadyUsedException.java`

```java
package tz.ac.suza.wt.studentsapi.exception;

public class EmailAlreadyUsedException extends RuntimeException {
    public EmailAlreadyUsedException(String email) {
        super("Email '" + email + "' is already used by another student");
    }
}
```

> 💡 **Teach moment.** Custom exceptions read like English at the call site:
> `throw new StudentNotFoundException(id);` is much clearer than `throw new RuntimeException("not found: " + id);`. They also become the **keys** for our error mapping in STEP 11.

### STEP 10 — Write the `StudentService`

Create `service/StudentService.java`:

```java
package tz.ac.suza.wt.studentsapi.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tz.ac.suza.wt.studentsapi.dto.StudentCreateDto;
import tz.ac.suza.wt.studentsapi.dto.StudentResponseDto;
import tz.ac.suza.wt.studentsapi.dto.StudentUpdateDto;
import tz.ac.suza.wt.studentsapi.entity.Student;
import tz.ac.suza.wt.studentsapi.exception.EmailAlreadyUsedException;
import tz.ac.suza.wt.studentsapi.exception.StudentNotFoundException;
import tz.ac.suza.wt.studentsapi.repository.StudentRepository;

import java.time.Instant;
import java.util.List;

@Service
@Transactional
public class StudentService {

    private final StudentRepository repository;

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

    @Transactional(readOnly = true)
    public List<StudentResponseDto> findAll() {
        return repository.findAll().stream()
                .map(StudentResponseDto::from)
                .toList();
    }

    @Transactional(readOnly = true)
    public List<StudentResponseDto> findByCourse(String course) {
        return repository.findByCourse(course).stream()
                .map(StudentResponseDto::from)
                .toList();
    }

    @Transactional(readOnly = true)
    public StudentResponseDto findById(Long id) {
        Student s = repository.findById(id)
                .orElseThrow(() -> new StudentNotFoundException(id));
        return StudentResponseDto.from(s);
    }

    public StudentResponseDto create(StudentCreateDto dto) {
        if (repository.existsByEmail(dto.email())) {
            throw new EmailAlreadyUsedException(dto.email());
        }
        Student s = Student.builder()
                .name(dto.name())
                .email(dto.email())
                .course(dto.course())
                .year(dto.year())
                .createdAt(Instant.now())
                .build();
        return StudentResponseDto.from(repository.save(s));
    }

    public StudentResponseDto update(Long id, StudentUpdateDto dto) {
        Student s = repository.findById(id)
                .orElseThrow(() -> new StudentNotFoundException(id));

        if (!s.getEmail().equals(dto.email())
                && repository.existsByEmail(dto.email())) {
            throw new EmailAlreadyUsedException(dto.email());
        }

        s.setName(dto.name());
        s.setEmail(dto.email());
        s.setCourse(dto.course());
        s.setYear(dto.year());
        return StudentResponseDto.from(s);
    }

    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new StudentNotFoundException(id);
        }
        repository.deleteById(id);
    }
}
```

> 💡 **Teach moment.**
> - `@Service` marks this as a Spring bean. The container constructs it once.
> - **Constructor injection** (not `@Autowired` on a field). Two reasons: it's testable without Spring, and the field is `final`.
> - `@Transactional` on the class means every method runs inside one DB transaction. Read methods are tagged `@Transactional(readOnly = true)` so Hibernate can skip the dirty-checking — a real performance win.
> - Notice `update(...)` calls `s.setName(...)` and **never calls `repository.save(...)`**. Because we're inside a transaction and `s` is a managed entity, Hibernate auto-flushes changes at commit. This is called **dirty checking**.

### STEP 11 — Write the `StudentController`

Create `controller/StudentController.java`:

```java
package tz.ac.suza.wt.studentsapi.controller;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tz.ac.suza.wt.studentsapi.dto.StudentCreateDto;
import tz.ac.suza.wt.studentsapi.dto.StudentResponseDto;
import tz.ac.suza.wt.studentsapi.dto.StudentUpdateDto;
import tz.ac.suza.wt.studentsapi.service.StudentService;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/api/students")
public class StudentController {

    private final StudentService service;

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

    @GetMapping
    public ResponseEntity<List<StudentResponseDto>> list(
            @RequestParam(required = false) String course) {
        List<StudentResponseDto> students = (course == null)
                ? service.findAll()
                : service.findByCourse(course);
        return ResponseEntity.ok(students);
    }

    @GetMapping("/{id}")
    public ResponseEntity<StudentResponseDto> getOne(@PathVariable Long id) {
        return ResponseEntity.ok(service.findById(id));
    }

    @PostMapping
    public ResponseEntity<StudentResponseDto> create(
            @Valid @RequestBody StudentCreateDto dto) {
        StudentResponseDto saved = service.create(dto);
        URI location = URI.create("/api/students/" + saved.id());
        return ResponseEntity.created(location).body(saved);
    }

    @PutMapping("/{id}")
    public ResponseEntity<StudentResponseDto> update(
            @PathVariable Long id,
            @Valid @RequestBody StudentUpdateDto dto) {
        return ResponseEntity.ok(service.update(id, dto));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.noContent().build();
    }
}
```

> 💡 **Teach moment.**
> - `@RestController` = `@Controller` + `@ResponseBody`. Every return value is serialised to JSON by Jackson.
> - `@RequestMapping("/api/students")` is the common URL prefix. Method annotations add the verb + path suffix.
> - `@Valid` triggers bean validation **before** the method body runs. If validation fails, Spring throws `MethodArgumentNotValidException` — we'll catch that in STEP 11.
> - Notice every method returns `ResponseEntity`. That's how we control status codes (200, 201, 204, etc.) and headers like `Location`.
> - **Notice what isn't here.** No business rules. No DB calls. No `try`/`catch`. The controller is HTTP plumbing only.

### STEP 12 — Write the `GlobalExceptionHandler`

This is the single most important pattern in the lab. Create `exception/GlobalExceptionHandler.java`:

```java
package tz.ac.suza.wt.studentsapi.exception;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
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.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(StudentNotFoundException.class)
    public ResponseEntity<ProblemDetail> handleNotFound(
            StudentNotFoundException ex, HttpServletRequest req) {
        ProblemDetail body = ProblemDetail
                .forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        body.setTitle("Resource not found");
        body.setInstance(URI.create(req.getRequestURI()));
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler(EmailAlreadyUsedException.class)
    public ResponseEntity<ProblemDetail> handleConflict(
            EmailAlreadyUsedException ex, HttpServletRequest req) {
        ProblemDetail body = ProblemDetail
                .forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
        body.setTitle("Email conflict");
        body.setInstance(URI.create(req.getRequestURI()));
        return ResponseEntity.status(HttpStatus.CONFLICT).body(body);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ProblemDetail> handleValidation(
            MethodArgumentNotValidException ex, HttpServletRequest req) {
        Map<String, String> fieldErrors = new LinkedHashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(fe ->
                fieldErrors.put(fe.getField(), fe.getDefaultMessage()));

        ProblemDetail body = ProblemDetail
                .forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
        body.setTitle("Bad request");
        body.setInstance(URI.create(req.getRequestURI()));
        body.setProperty("errors", fieldErrors);
        return ResponseEntity.badRequest().body(body);
    }
}
```

> 💡 **Teach moment.**
> - `@RestControllerAdvice` makes this a **global** interceptor. Every controller's exceptions pass through here before reaching the client.
> - We use `ProblemDetail` — Spring 6's built-in **RFC 7807** error type. It's a standard JSON shape: `type`, `title`, `status`, `detail`, `instance`.
> - For validation errors, we attach the per-field messages under an `errors` property so the frontend can show them next to the right input.
> - **No stack traces leak.** Even if something deep in JPA throws an `EntityNotFoundException`, the client only sees clean JSON.

A successful POST will now look like:

```json
{
  "id": 4,
  "name": "Asha Juma",
  "email": "asha@example.com",
  "course": "BCS",
  "year": 2,
  "createdAt": "2026-06-15T08:00:00Z"
}
```

A POST with a bad email returns:

```json
{
  "type":     "about:blank",
  "title":    "Bad request",
  "status":   400,
  "detail":   "Validation failed",
  "instance": "/api/students",
  "errors": {
    "email": "email must be a valid address"
  }
}
```

### STEP 13 — Seed some data on startup

So the API isn't empty on first run, create `config/DataSeeder.java`:

```java
package tz.ac.suza.wt.studentsapi.config;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tz.ac.suza.wt.studentsapi.entity.Student;
import tz.ac.suza.wt.studentsapi.repository.StudentRepository;

import java.time.Instant;

@Configuration
public class DataSeeder {

    @Bean
    CommandLineRunner seed(StudentRepository repository) {
        return args -> {
            if (repository.count() > 0) return;

            repository.save(Student.builder()
                    .name("Asha Juma").email("asha@example.com")
                    .course("BCS").year(2).createdAt(Instant.now()).build());

            repository.save(Student.builder()
                    .name("Hassan Ali").email("hassan@example.com")
                    .course("BITAM").year(3).createdAt(Instant.now()).build());

            repository.save(Student.builder()
                    .name("Mwajuma Said").email("mwajuma@example.com")
                    .course("BCS").year(1).createdAt(Instant.now()).build());
        };
    }
}
```

> 💡 **Teach moment.** A `CommandLineRunner` bean runs once, right after Spring finishes wiring. Perfect for seeding demo data.

### STEP 14 — First run + first request

```bash
mvn spring-boot:run
```

In another terminal:

```bash
curl -s http://localhost:8080/api/students | jq
```

You should see the three seeded students.

🎉 **You have a working REST API.**

---

## 4. Test every endpoint

Open VS Code, install the **REST Client** extension, then create `requests.http`:

```http
@host = http://localhost:8080
@base = {{host}}/api/students

### 1. List all students
GET {{base}}
Accept: application/json


### 2. Create a new student (201 Created)
POST {{base}}
Content-Type: application/json

{
  "name": "Salma Khamis",
  "email": "salma@example.com",
  "course": "BITAM",
  "year": 2
}


### 3. Get one student by id
GET {{base}}/1
Accept: application/json


### 4. Filter by course (query param)
GET {{base}}?course=BCS
Accept: application/json


### 5. Update an existing student (200 OK)
PUT {{base}}/1
Content-Type: application/json

{
  "name": "Asha Juma",
  "email": "asha.juma@example.com",
  "course": "BCS",
  "year": 3
}


### 6. Delete a student (204 No Content)
DELETE {{base}}/2


### 7. NEGATIVE TEST — invalid email (expect 400 with field errors)
POST {{base}}
Content-Type: application/json

{
  "name": "Bad Email",
  "email": "not-an-email",
  "course": "BCS",
  "year": 2
}


### 8. NEGATIVE TEST — student not found (expect 404)
GET {{base}}/9999


### 9. NEGATIVE TEST — duplicate email (expect 409)
POST {{base}}
Content-Type: application/json

{
  "name": "Duplicate",
  "email": "asha@example.com",
  "course": "BCS",
  "year": 1
}
```

Click **Send Request** above each block. Demonstrate all four success codes (200, 201, 204) and all three error codes (400, 404, 409) live.

### Same tests with curl

```bash
curl -i -X POST http://localhost:8080/api/students \
     -H 'Content-Type: application/json' \
     -d '{"name":"Salma","email":"salma@example.com","course":"BCS","year":2}'

curl -i http://localhost:8080/api/students/1

curl -i -X DELETE http://localhost:8080/api/students/2

curl -i -X POST http://localhost:8080/api/students \
     -H 'Content-Type: application/json' \
     -d '{"name":"Bad","email":"not-an-email","course":"BCS","year":2}'
```

---

## 5. Swagger UI

We already added `springdoc-openapi` to `pom.xml`. If you generated through Initializr without it, add this dependency now:

```xml
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.6.0</version>
</dependency>
```

Restart the app, then open:

**http://localhost:8080/swagger-ui.html**

You should see every endpoint listed. Click any one → **Try it out** → fill in the body → **Execute**. Live request + response with no extra tools.

> 💡 **Teach moment.** Swagger UI is generated from the same controller code. No separate spec file. When the API changes, the docs change.

---

## 6. What students must submit

Per the lab brief, the deliverables are:

1. **A GitHub repo** containing the project (accepted via GitHub Classroom).
2. **`requests.http`** in the repo root, with the 5 CRUD requests + 1 negative test.
3. **Screenshot of Swagger UI** embedded in the `README.md`.

### Rubric (10 marks)

| Criterion                              | Marks |
|----------------------------------------|-------|
| Entity + repository                    | 1     |
| Controller + service                   | 3     |
| Validation + error handling            | 2     |
| Correct HTTP status codes (201, 204)   | 1     |
| Tested via `requests.http`             | 2     |
| Swagger UI live                        | 1     |
| **Total**                              | **10**|

---

## 7. Common errors and fixes

### "cannot find symbol: variable `log` / `builder` / getters"

Lombok plugin not installed in the IDE.

- IntelliJ: **Settings → Plugins → Marketplace → "Lombok"** → install → restart.
- VS Code: install the **Lombok Annotations Support** extension.
- Also enable annotation processing: **Settings → Build → Compiler → Annotation Processors → Enable**.

### Port 8080 already in use

```bash
# macOS / Linux
lsof -ti:8080 | xargs kill -9
# Windows PowerShell
Get-Process -Id (Get-NetTCPConnection -LocalPort 8080).OwningProcess | Stop-Process
```

Or change the port in `application.properties`:

```properties
server.port=8081
```

### `MethodArgumentNotValidException` reaches the browser as a 500

You forgot `@Valid` on the controller method. Add it:

```java
public ResponseEntity<...> create(@Valid @RequestBody StudentCreateDto dto) { ... }
```

### POST returns 200 instead of 201

You used `ResponseEntity.ok(...)`. Switch to:

```java
return ResponseEntity.created(URI.create("/api/students/" + saved.id())).body(saved);
```

### DELETE returns 200 with a body

You returned the deleted object. Use `noContent()`:

```java
return ResponseEntity.noContent().build();
```

### "Validation failed for query"

Your derived query method has a typo. `findByCourse` is fine; `findByCorse` will fail at startup with a clear error message — read it carefully.

### `Connection refused` to Postgres

The DB container isn't running.

```bash
docker compose ps                 # should say "healthy"
docker compose up -d              # start it
docker compose logs db --tail 30  # check for errors
```

If port 5432 is already taken (a system Postgres is running), either stop that one or change the host port:

```yaml
ports:
  - "5433:5432"      # then use jdbc:postgresql://localhost:5433/studentsdb
```

### `relation "students" does not exist`

`ddl-auto` isn't generating the table. Confirm:

```properties
spring.jpa.hibernate.ddl-auto=update
```

Then restart the app — Hibernate prints `create table students (...)` in the log when it works.

### Want to inspect the DB?

```bash
docker exec -it students-api-db psql -U studentsapi -d studentsdb
\dt                    # list tables
SELECT * FROM students;
\q
```

Or use DBeaver / pgAdmin pointed at `localhost:5432`, db `studentsdb`, user `studentsapi`.

### Tests fail with "port already in use"

Spring Boot tests start a context but on a random port by default. If you wrote `@SpringBootTest(webEnvironment = RANDOM_PORT)` and still hit this, kill any leftover Java processes.

---

## Next lab

**Lab 7 — Authentication & Security:** signup, login, JWT, role-based access, rate limiting. We'll add `spring-boot-starter-security` to this same project and protect every endpoint behind a token.
