REST API je skup adresa preko kojih druga aplikacija čita i menja podatke na serveru. U ovoj lekciji praviš API za listu školskih zadataka (dodavanje, pregled, označavanje kao urađeno, brisanje), deliš kod na kontroler i servis i praviš jednostavnu stranu koja koristi API.
Pre nego što počneš: potrebno predznanje – Spring Boot – prva aplikacija, izuzeci. Kako da učiš: radi u svom projektu korak po korak i posle svakog koraka pokreni server i proveri rezultat u pregledaču. Primeri su za Spring Boot 3 ili 4 i Javu 17+; svaki deo koda upiši u fajl naveden u komentaru na vrhu primera.
REST: resursi i HTTP metode
U REST stilu podaci su resursi sa svojim adresama (/api/zadaci, /api/zadaci/3), a vrsta akcije određuje se HTTP metodom:
Metoda i adresa
Akcija
Odgovor (status)
GET /api/zadaci
lista svih zadataka
200 OK + JSON niz
GET /api/zadaci/3
jedan zadatak
200 OK, ili 404 Not Found
POST /api/zadaci
novi zadatak (podaci u telu zahteva, JSON)
201 Created
PUT /api/zadaci/3
izmena zadatka
200 OK, ili 404
DELETE /api/zadaci/3
brisanje
204 No Content, ili 404
Status je trocifren broj koji kaže kako je zahtev prošao: 2xx – uspeh, 4xx – greška klijenta (pogrešan zahtev, ne postoji), 5xx – greška na serveru.
Model: klasa Zadatak
// src/main/java/rs/svetprogramiranja/zadaci/Zadatak.java
package rs.svetprogramiranja.zadaci;
import java.time.LocalDate;
public class Zadatak {
private Long id;
private String naslov;
private String predmet;
private LocalDate rok; // u JSON-u: "2026-11-20"
private boolean uradjen;
public Zadatak() { } // prazan konstruktor – potreban za pretvaranje iz JSON-a
public Zadatak(String naslov, String predmet, LocalDate rok) {
this.naslov = naslov;
this.predmet = predmet;
this.rok = rok;
}
// geteri i seteri – preko njih Spring čita i upisuje polja u JSON
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNaslov() { return naslov; }
public void setNaslov(String naslov) { this.naslov = naslov; }
public String getPredmet() { return predmet; }
public void setPredmet(String predmet) { this.predmet = predmet; }
public LocalDate getRok() { return rok; }
public void setRok(LocalDate rok) { this.rok = rok; }
public boolean isUradjen() { return uradjen; }
public void setUradjen(boolean uradjen) { this.uradjen = uradjen; }
}
Ovde je obična klasa (a ne record) jer će se polja menjati, a u sledećoj lekciji ista klasa postaje tabela u bazi.
Servis: logika i podaci
Kontroler treba samo da primi zahtev i vrati odgovor. Sam posao (čuvanje, traženje, pravila) radi servis – klasa označena sa @Service. Za sada se zadaci čuvaju u mapi u memoriji (nestaju kad se server ugasi):
// src/main/java/rs/svetprogramiranja/zadaci/ZadatakService.java
package rs.svetprogramiranja.zadaci;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Service;
@Service
public class ZadatakService {
// server istovremeno obrađuje više zahteva – zato mapa i brojač bezbedni za više niti
private final Map<Long, Zadatak> zadaci = new ConcurrentHashMap<>();
private final AtomicLong sledeciId = new AtomicLong(1);
public ZadatakService() {
// nekoliko primera da lista ne bude prazna
sacuvaj(new Zadatak("Uraditi zadatke 1-5", "Matematika", LocalDate.now().plusDays(2)));
sacuvaj(new Zadatak("Procitati lektiru", "Srpski", LocalDate.now().plusDays(7)));
}
private Zadatak sacuvaj(Zadatak z) {
z.setId(sledeciId.getAndIncrement()); // novi zadatak dobija sledeći broj
zadaci.put(z.getId(), z);
return z;
}
public List<Zadatak> svi() {
List<Zadatak> lista = new ArrayList<>(zadaci.values());
lista.sort(Comparator.comparing(Zadatak::getRok)); // najbliži rok prvi
return lista;
}
public Optional<Zadatak> nadji(long id) {
return Optional.ofNullable(zadaci.get(id));
}
public Zadatak dodaj(Zadatak z) {
return sacuvaj(z);
}
public Optional<Zadatak> izmeni(long id, Zadatak novi) {
Zadatak z = zadaci.get(id);
if (z == null) return Optional.empty();
z.setNaslov(novi.getNaslov());
z.setPredmet(novi.getPredmet());
z.setRok(novi.getRok());
z.setUradjen(novi.isUradjen());
return Optional.of(z);
}
public boolean obrisi(long id) {
return zadaci.remove(id) != null;
}
}
Kontroler i ubacivanje zavisnosti
Kontroler dobija servis kroz konstruktor – to je ubacivanje zavisnosti (dependency injection): Spring sam pravi jedan objekat servisa i predaje ga svima kojima treba. @RequestMapping("/api/zadaci") na klasi zadaje početak adrese za sve metode.
// src/main/java/rs/svetprogramiranja/zadaci/ZadatakController.java
package rs.svetprogramiranja.zadaci;
import java.util.List;
import org.springframework.http.HttpStatus;
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.RestController;
@RestController
@RequestMapping("/api/zadaci")
public class ZadatakController {
private final ZadatakService servis;
public ZadatakController(ZadatakService servis) { // Spring ubacuje servis
this.servis = servis;
}
@GetMapping
public List<Zadatak> svi() {
return servis.svi();
}
@GetMapping("/{id}")
public ResponseEntity<Zadatak> jedan(@PathVariable long id) {
return servis.nadji(id)
.map(ResponseEntity::ok) // 200 + zadatak
.orElse(ResponseEntity.notFound().build()); // 404
}
@PostMapping
public ResponseEntity<Zadatak> dodaj(@RequestBody Zadatak z) { // JSON iz tela zahteva -> objekat
if (z.getNaslov() == null || z.getNaslov().isBlank())
return ResponseEntity.badRequest().build(); // 400
return ResponseEntity.status(HttpStatus.CREATED).body(servis.dodaj(z)); // 201
}
@PutMapping("/{id}")
public ResponseEntity<Zadatak> izmeni(@PathVariable long id, @RequestBody Zadatak z) {
return servis.izmeni(id, z)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> obrisi(@PathVariable long id) {
return servis.obrisi(id)
? ResponseEntity.noContent().build() // 204
: ResponseEntity.notFound().build();
}
}
ResponseEntity omogućava da se uz podatke vrati i tačan status. Metode map i orElse su iste kao kod Optional-a u lekciji Lambda izrazi i Stream API.
Isprobavanje API-ja: curl
GET zahteve možeš da otvoriš u pregledaču, ali POST, PUT i DELETE šalje program. Najjednostavniji je curl, koji postoji i u Windows-u (komande za cmd; navodnici unutar JSON-a pišu se kao \"):
curl http://localhost:8080/api/zadaci
curl -X POST http://localhost:8080/api/zadaci -H "Content-Type: application/json" -d "{\"naslov\":\"Referat o Tesli\",\"predmet\":\"Fizika\",\"rok\":\"2026-11-25\"}"
curl -X PUT http://localhost:8080/api/zadaci/3 -H "Content-Type: application/json" -d "{\"naslov\":\"Referat o Tesli\",\"predmet\":\"Fizika\",\"rok\":\"2026-11-25\",\"uradjen\":true}"
curl -i -X DELETE http://localhost:8080/api/zadaci/1
curl -i http://localhost:8080/api/zadaci/99
Opcija -i prikazuje i status odgovora (npr. HTTP/1.1 404). Udobniji su grafički alati kao Postman ili Bruno, ili ugrađeni HTTP klijent u IntelliJ IDEA (fajl .http).
Strana koja koristi API
Strana u static/index.html čita listu i šalje nove zadatke pomoću fetch:
Zadaci za sada nestaju kada se server ponovo pokrene. U sledećoj lekciji se čuvaju u bazi podataka.
Zadaci za vežbu
Za svaki zadatak postoji primer, uputstvo i rešenje.
ZADATAK 01Težina: srednje
Filtriranje i statistika
Dopuni API:
GET /api/zadaci?predmet=Fizika vraća samo zadatke iz tog predmeta (bez obzira na velika i mala slova); bez parametra vraća sve;
GET /api/zadaci/statistika vraća JSON oblika {"ukupno":5,"uradjeno":2,"kasni":1}, gde su „kasni“ neurađeni zadaci kojima je rok prošao.
Uputstvo
U metodi svi dodaj @RequestParam(required = false) String predmet i filtriraj streamom. Za statistiku napravi record Statistika(long ukupno, long uradjeno, long kasni) i vrati ga iz nove metode sa @GetMapping("/statistika"). Datum poredi sa rok.isBefore(LocalDate.now()).
Rešenje (Java)
// u klasi ZadatakController (izmene i dodaci)
// importi: java.time.LocalDate, org.springframework.web.bind.annotation.RequestParam
public record Statistika(long ukupno, long uradjeno, long kasni) { }
@GetMapping
public List<Zadatak> svi(@RequestParam(required = false) String predmet) {
if (predmet == null || predmet.isBlank())
return servis.svi();
return servis.svi().stream()
.filter(z -> predmet.equalsIgnoreCase(z.getPredmet()))
.toList();
}
@GetMapping("/statistika")
public Statistika statistika() {
List<Zadatak> svi = servis.svi();
long uradjeno = svi.stream().filter(Zadatak::isUradjen).count();
long kasni = svi.stream()
.filter(z -> !z.isUradjen() && z.getRok() != null && z.getRok().isBefore(LocalDate.now()))
.count();
return new Statistika(svi.size(), uradjeno, kasni);
}
Česte greške i saveti
Ruta /statistika i ruta /{id} se ne sudaraju: Spring prvo bira tačnu putanju, pa tek onda šablon sa promenljivom.
Za POST i PUT u zahtevu mora biti zaglavlje Content-Type: application/json, inače server vraća grešku 415.
Klasa koja se prima iz JSON-a mora imati prazan konstruktor i setere (ili biti record).
Ne prikazuj tekst iz baze ili od korisnika sa innerHTML – koristi textContent, da neko ne bi ubacio svoj HTML ili JavaScript.
Proveri znanje
Otvori kviz
REST API
1. Koja HTTP metoda se koristi za dodavanje novog zadatka?
2. Šta znači status 404?
3. Čemu služi @RequestBody?
4. Kako kontroler dobija objekat servisa?
Podešavanja kolačića
Koristimo kolačiće da bismo vam pružili najbolje moguće iskustvo. Takođe nam omogućavaju da analiziramo ponašanje korisnika kako bismo stalno poboljšavali veb lokaciju za vas.