# Lab 6 Walkthrough — Beginner Edition

> **For:** students seeing Spring Boot for the first time.
> **Length:** ~90 minutes, copy-paste-and-explain.
> **What we'll build:** a tiny REST API with **three** classes — Entity, Repository, Controller — that stores students in PostgreSQL.

This is the friendlier version of Lab 6. We skip DTOs, the service layer, global exception handling, Docker, Swagger, and tests. Once this works and feels familiar, graduate to the production-grade [full walkthrough](lab06_walkthrough.md).

Finished project: [examples/lab06_students_api_simple/](../examples/lab06_students_api_simple/).

---

## The whole picture

```
HTTP request
     │
     ▼
┌─────────────────────┐
│ StudentController   │  ← 5 endpoints
└─────────────────────┘
     │
     ▼
┌─────────────────────┐
│ StudentRepository   │  ← extends JpaRepository → free CRUD methods
└─────────────────────┘
     │
     ▼
   PostgreSQL          ← students table
```

Three classes. That's it.

| Class            | Lines of code | What it does |
|------------------|---------------|--------------|
| `Student`        | ~50           | one row in the `students` table |
| `StudentRepository` | 4          | empty — JPA generates everything |
| `StudentController` | ~50        | the 5 HTTP endpoints |

---

## STEP 0 — Before we start (5 min)

You need three things to be working:

```bash
java -version          # 21
mvn -version           # 3.9+
psql --version         # any 14+
```

If `java` or `mvn` are missing on Windows, do [windows_jdk_maven_setup.md](../resources/windows_jdk_maven_setup.md) first.

For PostgreSQL:

- **macOS:** install Postgres.app from https://postgresapp.com
- **Windows:** installer at https://www.postgresql.org/download/windows/
- **Ubuntu:** `sudo apt install postgresql`

Then create the lab database and user (one time):

```bash
psql -d postgres -c "CREATE USER studentsapi WITH PASSWORD 'studentsapi';"
psql -d postgres -c "CREATE DATABASE studentsdb OWNER studentsapi;"
```

> 💡 **Why a separate user?** Real apps never connect as the database admin. We give the app its own user with access only to its own database.

---

## STEP 1 — Generate the project (5 min)

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

| Field         | Value                |
|---------------|----------------------|
| Project       | **Maven**            |
| Language      | **Java**             |
| Spring Boot   | **3.3.4**            |
| Group         | `tz.ac.suza.wt`      |
| Artifact      | `students-api`       |
| Java          | **21**               |

Click **ADD DEPENDENCIES** and tick exactly **four**:

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

Click **GENERATE**, unzip, open the folder in your editor.

> 💡 **What did we just tick?**
> - **Spring Web** — listens for HTTP requests.
> - **Spring Data JPA** — the database layer.
> - **Validation** — checks `@NotBlank`, `@Email`, etc. before our code runs.
> - **PostgreSQL Driver** — Java talking to Postgres.

---

## STEP 2 — Configure the database connection (3 min)

Open `src/main/resources/application.properties` and replace its contents with:

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

spring.datasource.url=jdbc:postgresql://localhost:5432/studentsdb
spring.datasource.username=studentsapi
spring.datasource.password=studentsapi

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
```

> 💡 **What does each line mean?**
> - The **`datasource.*`** lines tell Spring where Postgres is and how to log in.
> - `ddl-auto=update` lets Hibernate **create the table automatically** the first time the app starts. Magic, but only safe in development.
> - `show-sql=true` prints every SQL statement to the console. Great for learning what's actually happening.

---

## STEP 3 — Write the entity (10 min)

Create the package `entity` and inside it the file `Student.java`:

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

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
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;

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

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

    @NotBlank
    private String name;

    @NotBlank
    @Email
    private String email;

    @NotBlank
    private String course;

    @NotNull
    @Min(1)
    @Max(4)
    private Integer year;

    // JPA needs an empty constructor
    public Student() {}

    public Student(String name, String email, String course, Integer year) {
        this.name = name;
        this.email = email;
        this.course = course;
        this.year = year;
    }

    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 String getCourse() { return course; }
    public void setCourse(String course) { this.course = course; }

    public Integer getYear() { return year; }
    public void setYear(Integer year) { this.year = year; }
}
```

> 💡 **Read this carefully — it's the heart of the lab.**
> - `@Entity` + `@Table("students")` = "this class is a database table called `students`."
> - `@Id` + `@GeneratedValue(IDENTITY)` = the database generates the id for us.
> - `@NotBlank`, `@Email`, `@Min`, `@Max` = validation rules. If a client sends bad data, Spring rejects it **before** this code runs.
> - The empty `public Student()` constructor is mandatory — JPA uses reflection to create new instances when reading from the database.
> - Getters and setters look long, but your IDE can generate them: right-click in the class → **Generate → Getters and Setters** (IntelliJ) or use the Lombok extension in VS Code.

---

## STEP 4 — Write the repository (2 min)

Create the package `repository` and inside it `StudentRepository.java`:

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

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

public interface StudentRepository extends JpaRepository<Student, Long> {
}
```

That's the whole file. **Four lines** and we get `findAll()`, `findById()`, `save()`, `deleteById()`, `count()` for free.

> 💡 **What just happened?** `JpaRepository<Student, Long>` means "give me CRUD methods for the `Student` entity whose id is `Long`." Spring writes the SQL for us at startup.

---

## STEP 5 — Write the controller (15 min)

Create the package `controller` and inside it `StudentController.java`:

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

import jakarta.validation.Valid;
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.RestController;
import tz.ac.suza.wt.studentsapi.entity.Student;
import tz.ac.suza.wt.studentsapi.repository.StudentRepository;

import java.util.List;

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

    private final StudentRepository repository;

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

    // GET /api/students -> list all students
    @GetMapping
    public List<Student> findAll() {
        return repository.findAll();
    }

    // GET /api/students/1 -> one student
    @GetMapping("/{id}")
    public Student findOne(@PathVariable Long id) {
        return repository.findById(id).orElseThrow();
    }

    // POST /api/students -> create
    @PostMapping
    public Student create(@Valid @RequestBody Student student) {
        return repository.save(student);
    }

    // PUT /api/students/1 -> update
    @PutMapping("/{id}")
    public Student update(@PathVariable Long id, @Valid @RequestBody Student updated) {
        updated.setId(id);
        return repository.save(updated);
    }

    // DELETE /api/students/1 -> delete
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        repository.deleteById(id);
    }
}
```

> 💡 **Five endpoints, one per HTTP verb.**
> - `@RestController` = "everything I return is JSON."
> - `@RequestMapping("/api/students")` = "all my methods live under this URL."
> - `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping` map a method to one HTTP verb.
> - `@PathVariable Long id` pulls `1` out of `/api/students/1`.
> - `@RequestBody Student student` converts the incoming JSON into a `Student` object.
> - `@Valid` runs the validation annotations from STEP 3 first. Bad data → automatic 400 response.

---

## STEP 6 — Run it (5 min)

In a terminal in the project folder:

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

Watch the logs. You should see Hibernate print something like:

```sql
Hibernate: create table students (
    id bigserial not null,
    course varchar(255) not null,
    email varchar(255) not null,
    name varchar(255) not null,
    year integer not null,
    primary key (id)
)
```

That's the table being created automatically. **You didn't write any SQL.**

Open another terminal and try:

```bash
# List (empty at first)
curl http://localhost:8080/api/students

# Create
curl -X POST http://localhost:8080/api/students \
  -H 'Content-Type: application/json' \
  -d '{"name":"Asha","email":"asha@example.com","course":"BCS","year":2}'

# List again — now has one student
curl http://localhost:8080/api/students

# Get one
curl http://localhost:8080/api/students/1

# Update
curl -X PUT http://localhost:8080/api/students/1 \
  -H 'Content-Type: application/json' \
  -d '{"name":"Asha Juma","email":"asha.j@example.com","course":"BCS","year":3}'

# Delete
curl -X DELETE http://localhost:8080/api/students/1
```

🎉 **You have a working REST API backed by Postgres.**

---

## STEP 7 — See validation reject bad data (3 min)

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

You get a `400 Bad Request`. The annotations did their job — your controller never ran.

> 💡 **No `if`-statements.** This is the power of declarative validation.

---

## STEP 8 — See the data in Postgres directly (3 min)

```bash
psql -U studentsapi -d studentsdb
```

Inside `psql`:

```sql
\dt                  -- list tables
SELECT * FROM students;
\q                   -- quit
```

The data Spring saved is sitting in real Postgres rows. The "API" is just a thin web layer over that.

---

## What we skipped (and where to find it)

This beginner version returned the **entity directly** to clients. That's the simplest path, and it's fine for learning. Real production APIs add more:

| Production pattern              | What it adds                                    | Where to learn it |
|---------------------------------|-------------------------------------------------|-------------------|
| **DTOs** (separate input/output objects) | Stops leaking database columns; safer | [full walkthrough STEP 8](lab06_walkthrough.md#step-8--write-the-three-dtos) |
| **Service layer**               | Keeps business logic out of the controller      | [full walkthrough STEP 10](lab06_walkthrough.md#step-10--write-the-studentservice) |
| **`@RestControllerAdvice`**     | Clean error JSON instead of stack traces        | [full walkthrough STEP 12](lab06_walkthrough.md#step-12--write-the-globalexceptionhandler) |
| **Swagger UI**                  | Auto-generated docs at `/swagger-ui.html`       | [full walkthrough STEP 5](lab06_walkthrough.md#5-swagger-ui) |
| **Tests**                       | MockMvc tests in `src/test`                     | [reference project](../examples/lab06_students_api/src/test/) |
| **Docker for Postgres**         | One-command DB, no install ceremony             | [full walkthrough STEP 2](lab06_walkthrough.md#step-2--start-a-local-postgres-with-docker) |

When this beginner version feels easy, do the full walkthrough next. Same problem, every concept added one at a time.

---

## Common errors

### `connection refused` on startup

Postgres isn't running. Start Postgres.app (macOS), the Postgres service (Windows), or `sudo systemctl start postgresql` (Linux).

### `FATAL: password authentication failed for user "studentsapi"`

You skipped STEP 0's `CREATE USER` step, or used a different password. Re-run:

```bash
psql -d postgres -c "DROP USER IF EXISTS studentsapi;"
psql -d postgres -c "CREATE USER studentsapi WITH PASSWORD 'studentsapi';"
psql -d postgres -c "DROP DATABASE IF EXISTS studentsdb;"
psql -d postgres -c "CREATE DATABASE studentsdb OWNER studentsapi;"
```

### `port 5432 is already in use` / `bind: address already in use`

Another Postgres is running. Either stop it, or change the port in `application.properties` (e.g. `5433`).

### `mvn` reports the wrong Java version

`JAVA_HOME` is wrong. See [windows_jdk_maven_setup.md](../resources/windows_jdk_maven_setup.md) (Windows) or `export JAVA_HOME=$(/usr/libexec/java_home -v 21)` on macOS.

### `NoSuchElementException` when GETting an id that doesn't exist

Our `findOne()` uses `.orElseThrow()` — it throws when the id isn't found. The full walkthrough wraps this in a clean 404. For now, just don't ask for ids that don't exist 🙂.
