Fullstack CRUD Handbook (Java 17 · Spring Boot · React · Node.js · Bootstrap 5 · JS)
Java 17 Spring Boot React Node.js/Express Bootstrap 5 ESNext

Jak tuhle příručku používat

Je to „kapesní“ referenční manuál s copy‑ready příkazy a šablonami. Otevři levé menu, n.jsš, co hledáš (např. vite, jwt, docker), a skrolni na sekci. V kódech klikni na Kopírovat.

Pozn.: Některé způsoby můžou mít víc variant. U projektu React dnes často používáme Vite (rychlejší dev server). create‑react‑app je pořád použitelné, ale považované za „legacy“. Zahrnuji obě cesty.

0) Předpoklady a instalace

Java 17

  • Ověř: java -version
  • SDKMAN (macOS/Linux): curl -s "https://get.sdkman.io" | bash
  • Instalace: sdk install java 17.0.10-tem

Node.js (LTS)

  • Ověř: node -v, npm -v
  • Volitelně nvm: nvm install --lts

Nástroje

  • Git: git --version
  • IDE: IntelliJ IDEA, VS Code
  • HTTP klient: curl/Postman/Insomnia

1) React — založení projektu

Varianta A — Vite (doporučeno)


npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev # spuštění vývoje
npm run build # produkční build
npm run preview # lokální náhled buildu

Alternativy: --template react-swc, TypeScript: --template react-ts.

Varianta B — create-react-app (legacy, ale funguje)

npx create-react-app my-react-app
cd my-react-app
npm start
npm run build

Základní komponenta


import { useEffect, useState } from "react";
export default function App() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    (async () => {
      try {
        setLoading(true);
        const res = await fetch("/api/users");
        if (!res.ok) throw new Error("HTTP " + res.status);
        setUsers(await res.json());
      } catch (e) { setError(e.message); }
      finally { setLoading(false); }
    })();
  }, []);

  if (loading) return 

Načítám…

; if (error) return

Chyba: {error}

; return (

Uživatelé

    {users.map(u =>
  • {u.name} ({u.email})
  • )}
); }

Router (react-router-dom)

npm install react-router-dom

// main.jsx
import { createRoot } from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import App from './App'
import UserDetail from './UserDetail'

const router = createBrowserRouter([
  { path: '/', element:  },
  { path: '/users/:id', element:  }
])
createRoot(document.getElementById('root')).render()

Formulář + controlled inputs

function UserForm({ onSave, initial }) {
  const [form, setForm] = useState(initial ?? { name: '', email: '' })
  const submit = e => { e.preventDefault(); onSave(form) }
  return (
    <form onSubmit={submit}>
      <input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
      <input value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} />
      <button>Uložit</button>
    </form>
  )
}

Volání API — fetch & Axios

// fetch
await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(user) })

// axios
import axios from 'axios'
const api = axios.create({ baseURL: '/api' })
const res = await api.get('/users')

Přidání Bootstrapu 5 do Reactu

npm install bootstrap
// main.jsx
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'

1.1) React základní setup

Zobrazit data z API (fetch)


                    
                    //api.js
                    export async function apiGet(endpoint) {
    const url = `https://rickandmortyapi.com/api/${endpoint}`;

    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Error fetching data:", error);
        throw error; // Rethrow the error to be handled by the caller
    }
}

1.2) Characters

Funkce pro postavy (načítaní listu)

//Characters.jsx
import {useEffect, useState} from "react";
import {apiGet} from "../utils/api.js";
import {Link} from "react-router-dom";

export default function Characters() {
    const [characters, setCharacters] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        async function fetchCharacter() {
            try {
                const data = await apiGet("character");
                setCharacters(data.results || []); // ✅ pole
                console.log(data);
            } catch (e) {
                console.error(e + "Error fetching characters");
                setError(e.message + "Error fetching characters");
            } finally {
                setLoading(false);
            }
        }

        fetchCharacter();
    }, []);

    if (loading) return <h2>Načítám...</h2>;
    if (error) return <h2>Chyba: {error}</h2>;

    return (
        <div>
            <h1>Postavy</h1>

            {characters.map((character) => (
                <div key={character.id}>
                    <h4>
                        <Link to={`/postavy/${character.id}`}>
                            {character.name}
                        </Link>
                    </h4>

                    <img
                      className="character"
                      src={character.image}
                      alt={character.name}
                    />
                </div>
            ))}
        </div>
    );
}

1.3) Epizody postavy

Funkce pro epizody postavy (načítaní jednoho)

//EpisodeDetail.jsx
import {useState, useEffect} from 'react';
import {Link, useParams} from "react-router-dom";
import {apiGet} from "../utils/api.js";

export default function EpisodeDetail() {
    const {id} = useParams();
    const [episode, setEpisode] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        async function load() {
            try {
                setLoading(true);
                const data = await apiGet(`episode/${id}`);
                setEpisode(data || []); // ✅ pole
                console.log(data);
            } catch (e) {
                console.error(e + "Error fetching episode");
                setError(e.message + "Error fetching episode");
            } finally {
                setLoading(false);
            }
        }
        load();
    }, [id]);

    if (loading) return <h2>Načítám…</h2>;
    if (error) return <h2>Chyba: {error}</h2>;
    if (!episode) return <h2>Epizoda nenalezena</h2>;

    return (
        <div>
            <h1>{episode.episode} — {episode.name}</h1>

            <h3>Postavy v epizodě</h3>

            <ul>
                {episode.characters.slice(0, 20).map((url) => {
                    const characterId = url.split("/").pop();

                    return (
                        <li key={characterId}>
                            <img
                              src={`https://rickandmortyapi.com/api/character/avatar/${characterId}.jpeg`}
                              alt={`Postava ${characterId}`}
                              width={50}
                              height={50}
                            />
                            <Link to={`/postavy/${characterId}`}>
                                Postava {characterId}
                            </Link>
                        </li>
                    );
                })}
            </ul>
        </div>
    );
}

1.4) Detail postavy

Funkce pro detailní informace postavy (načítaní jednoho plus načtení epizod)

//Detail.jsx
import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom";
import { apiGet } from "../utils/api.js";

export default function Detail() {
    const { id } = useParams();
    const [character, setCharacter] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [episodes, setEpisodes] = useState([]);

    useEffect(() => {
        async function load() {
            try {
                setLoading(true);
                setError(null);

                const data = await apiGet(`character/${id}`); // 👈 použiješ id
                setCharacter(data);

                const episodeIds = data.episode.map((url) => url.split("/").pop());
                if (episodeIds.length > 0) {
                    const epData = await apiGet(`episode/${episodeIds.join(",")}`);
                    setEpisodes(Array.isArray(epData) ? epData : [epData]);
                }
            } catch (e) {
                setError(e.message);
            } finally {
                setLoading(false);
            }
        }

        load();
    }, [id]);

    if (loading) return <h2>Načítám…</h2>;
    if (error) return <h2>Chyba: {error}</h2>;
    if (!character) return <h2>Postava nenalezena</h2>;

    return (
        <div className="detail">
            <Link to="/postavy">⬅ Zpět</Link>

            <h1>{character.name}</h1>

            <img src={character.image} alt={character.name} />

            <p>Status: {character.status}</p>
            <p>Species: {character.species}</p>
            <p>Gender: {character.gender}</p>
            <p>Origin: {character.origin?.name}</p>

            {episodes.length > 0 && (
                <>
                    <h3>Epizody&


                        
    

1.5) APP jsx pro Ricky and Morthy

Router dome

//App.jsx
import './App.css'
import {BrowserRouter, Link, Route, Routes} from "react-router-dom";
import Home from "./RickAndMorty/Home.jsx";
import Detail from "./RickAndMorty/Detail.jsx";
import Characters from "./RickAndMorty/Characters.jsx";
import EpisodeDetail from "./RickAndMorty/EpisodeDetail.jsx";

function App() {

    return (
        <>
            <BrowserRouter>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/postavy" element={<Characters />} />
                    <Route path="/postavy/:id" element={<Detail />} />
                    <Route path="/epizody/:id" element={<EpisodeDetail />} />
                </Routes>
            </BrowserRouter>
        </>
    )
}

export default App

2) Node.js / Express — rychlé API

Skeleton

npm init -y
npm install express cors dotenv zod

// index.js
const express = require('express')
const cors = require('cors')
const { z } = require('zod')
require('dotenv').config()

const app = express()
app.use(cors())
app.use(express.json())

let users = []
const User = z.object({ id: z.number().optional(), name: z.string().min(1), email: z.string().email() })

app.get('/api/users', (req, res) => res.json(users))
app.post('/api/users', (req, res) => {
  const parsed = User.safeParse(req.body)
  if (!parsed.success) return res.status(400).json(parsed.error)
  const user = { id: Date.now(), ...parsed.data }
  users.push(user)
  res.status(201).json(user)
})
app.put('/api/users/:id', (req, res) => {
  const id = Number(req.params.id)
  const i = users.findIndex(u => u.id === id)
  if (i < 0) return res.sendStatus(404)
  users[i] = { ...users[i], ...req.body }
  res.json(users[i])
})
app.delete('/api/users/:id', (req, res) => {
  users = users.filter(u => u.id !== Number(req.params.id))
  res.sendStatus(204)
})

const port = process.env.PORT || 3000
app.listen(port, () => console.log('API listening on', port))

Užitečné skripty

// package.json
{
  "type": "module", // pro ES moduly (volitelné)
  "scripts": {
    "dev": "node index.js",
    "start": "NODE_ENV=production node index.js"
  }
}

Pozn.: Na Windows použij knihovnu cross-env pro nastavování env proměnných.

3) Spring Boot — plný CRUD (Java 17)

Vytvoření projektu

  • Přes Spring Initializr: závislosti Spring Web, Spring Data JPA, Validation, Spring Security (volitelně), a driver DB (PostgreSQL/MySQL).
  • Příkazem (Maven): mvn -v; build & run: mvn spring-boot:run

application.properties (PostgreSQL)

spring.datasource.url=jdbc:postgresql://localhost:5432/app
spring.datasource.username=app
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# CORS (globální) — pro vývoj povolíme vše, v produkci upřesnit!
app.cors.allowed-origins=http://localhost:5173,http://localhost:3000

Konfigurace CORS (Java)

@Configuration
public class CorsConfig {
  @Bean
  public WebMvcConfigurer corsConfigurer(@Value("${app.cors.allowed-origins:*") String origins) {
    return new WebMvcConfigurer() {
      @Override public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedMethods("*").allowedOrigins(origins.split(","));
      }
    };
  }
}

Entity + Validation

@Entity @Table(name = "users")
public class User {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @NotBlank @Column(nullable = false)
  private String name;

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

  // get/set/ctor
}

Repository + Service

public interface UserRepository extends JpaRepository<User, Long> {
  Optional<User> findByEmail(String email);
}

@Service
public class UserService {
  private final UserRepository repo;
  public UserService(UserRepository repo) { this.repo = repo; }

  public Page<User> list(Pageable pageable) { return repo.findAll(pageable); }
  public User create(@Valid User u) { return repo.save(u); }
  public User update(Long id, User patch) {
    User u = repo.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    if (patch.getName()!=null) u.setName(patch.getName());
    if (patch.getEmail()!=null) u.setEmail(patch.getEmail());
    return repo.save(u);
  }
  public void delete(Long id) { repo.deleteById(id); }
}

REST Controller (CRUD + stránkování)

@RestController @RequestMapping("/api/users")
public class UserController {
  private final UserService svc;
  public UserController(UserService svc) { this.svc = svc; }

  @GetMapping
  public Page<User> list(@PageableDefault(size=20, sort="id", direction = Sort.Direction.DESC) Pageable p) {
    return svc.list(p);
  }

  @PostMapping @ResponseStatus(HttpStatus.CREATED)
  public User create(@Valid @RequestBody User u) { return svc.create(u); }

  @PutMapping("/{id}")
  public User update(@PathVariable Long id, @RequestBody User patch) { return svc.update(id, patch); }

  @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT)
  public void delete(@PathVariable Long id) { svc.delete(id); }
}

Global exception handler

@RestControllerAdvice
public class ApiErrors {
  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  Map<String,Object> handleValidation(MethodArgumentNotValidException ex) {
    Map<String,Object> body = new HashMap<>();
    body.put("error", "validation_error");
    body.put("fields", ex.getBindingResult().getFieldErrors()
        .stream().collect(Collectors.toMap(FieldError::getField, DefaultMessageSourceResolvable::getDefaultMessage, (a,b) -> a)));
    return body;
  }
}

Spring Security — minimální JWT (schéma)

// build.gradle/maven: přidej jjwt-api a impl
// Konfigurace SecurityFilterChain – povol /auth/**, chraň /api/**, filtr pro ověření JWT v Authorization: Bearer <token>.
// V produkci používej rotaci klíčů a krátkou expiraci.

Záměrně zkráceno – JWT má mnoho detailů (klíče, expirace, refresh). V této příručce držíme „minimum viable“ vzor.

4) Bootstrap 5 — layout, formuláře, komponenty

Rychlý start (CDN)

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

Grid a formulář

<div class="container">
  <div class="row g-3">
    <div class="col-md-6">
      <label class="form-label">Jméno</label>
      <input class="form-control" />
    </div>
    <div class="col-md-6">
      <label class="form-label">Email</label>
      <input type="email" class="form-control" />
    </div>
  </div>
  <button class="btn btn-primary mt-3">Uložit</button>
</div>

Tabulka

<table class="table table-striped table-hover">
  <thead><tr><th>ID</th><th>Jméno</th><th>Email</th></tr></thead>
  <tbody id="tbl"></tbody>
</table>

Modal

<button class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#m">Otevřít</button>
<div class="modal fade" id="m" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">...</div>
  </div>
</div>

Toast

<div class="toast-container position-fixed bottom-0 end-0 p-3">
  <div id="t" class="toast"><div class="toast-body">Uloženo!</div></div>
</div>
<script>new bootstrap.Toast(document.getElementById('t')).show()</script>

5) JavaScript — praktické vzory

Fetch helper

async function api(path, init) {
  const res = await fetch('/api' + path, { headers: { 'Content-Type': 'application/json' }, ...init })
  if (!res.ok) throw new Error('HTTP ' + res.status)
  return res.headers.get('content-type')?.includes('json') ? res.json() : res.text()
}
// použití: await api('/users')

Debounce

const debounce = (fn, ms = 300) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms) } }

FormData → JSON

const toJson = (form) => Object.fromEntries(new FormData(form).entries())

6) REST konvence, stránkování, filtrování

Adresy a metody

  • GET /api/users — list (filter, page, size, sort)
  • POST /api/users — create
  • GET /api/users/{id} — detail
  • PUT /api/users/{id} — update
  • DELETE /api/users/{id} — delete

cURL příklady

curl -X POST http://localhost:8080/api/users -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"a@b.cz"}'

curl 'http://localhost:8080/api/users?page=0&size=20&sort=id,desc'

7) Databáze a migrace (PostgreSQL)

Rychlý start

# Docker
docker run --name pg -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=app -p 5432:5432 -d postgres:16

Flyway migrace (Maven)

# src/main/resources/db/migration/V1__init.sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

# pom.xml: <dependency> org.flywaydb:flyway-core </dependency>
# application.properties: spring.flyway.enabled=true

8) Testování

Spring — JUnit 5

@SpringBootTest
class UserControllerTest {
  @Autowired MockMvc mvc;

  @Test void createUser() throws Exception {
    mvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON)
        .content("{\\"name\\":\\"A\\",\\"email\\":\\"a@b.cz\\"}"))
      .andExpect(status().isCreated());
  }
}

React — Jest + React Testing Library

import { render, screen } from '@testing-library/react'
import App from './App'

test('zobrazí nadpis', () => {
  render(<App />)
  expect(screen.getByText(/Uživatelé/i)).toBeInTheDocument()
})

9) Build & deploy (Docker)

Spring Boot Dockerfile

# Dockerfile (multi-stage)
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -q -e -DskipTests dependency:go-offline
COPY src ./src
RUN mvn -q -DskipTests package

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]

React (Vite) — statický build + Nginx

# docker build -t react-app .
# docker run -p 8081:80 react-app

# Dockerfile
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

docker-compose (API + DB + Frontend)

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    ports: ["5432:5432"]
  api:
    build: ./api
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/app
      SPRING_DATASOURCE_USERNAME: postgres
      SPRING_DATASOURCE_PASSWORD: secret
    ports: ["8080:8080"]
    depends_on: [db]
  web:
    build: ./web
    ports: ["5173:80"]
    depends_on: [api]

10) Troubleshooting checklist

  • 💥 CORS: povol správné Origin (viz CORS config výš).
  • 🔑 JWT: chybí Authorization: Bearer <token>.
  • 🗄️ DB: přístupová práva/port, správný driver a URL.
  • 🧱 404 na SPA routách: použij SPA fallback v Nginxu.
  • 🧪 Testy: nezapomeň na spring-boot-starter-test a @SpringBootTest.

11) Cheat‑sheet příkazy

# React (Vite)
npm create vite@latest myapp -- --template react
cd myapp && npm i && npm run dev
# React (CRA)
npx create-react-app myapp
cd myapp && npm start
# Spring Boot
mvn spring-boot:run
# nebo
./mvnw spring-boot:run
# Express skeleton
npm init -y
npm i express cors dotenv zod
node index.js
# Docker Postgres
docker run --name pg -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=app -p 5432:5432 -d postgres:16
# cURL test
curl http://localhost:8080/api/users

12) Mini‑glosář

  • CRUD — Create/Read/Update/Delete, základní operace nad daty.
  • SPA — Single Page Application (React, Vue, …)
  • CSR/SSR — Client/Server‑Side Rendering.
  • DTO — objekt pro přenos dat mezi vrstvami.
  • ORM — mapování objektů na tabulky (JPA/Hibernate).