Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

Dotarliśmy do finału serii! Przez 19 wpisów nauczyliśmy się wszystkich fundamentów React – od podstawowych komponentów, przez hooki, routing, stylowanie, optymalizację, TypeScript, zarządzanie stanem, komunikację z API, aż po autentykację.

Teraz czas połączyć WSZYSTKO w jedną kompletną Full Stack aplikację! Zbudujemy Task Manager (Menedżer Zadań) – aplikację do zarządzania projektami i zadaniami z pełną funkcjonalnością:

  • Backend: .NET Core Web API (REST API z Entity Framework)
  • Frontend: React + TypeScript + Vite
  • Zarządzanie stanem: Zustand (dla stanu globalnego)
  • Komunikacja z API: React Query (cache, mutations, optimistic updates)
  • Autentykacja: JWT tokens + httpOnly cookies
  • CRUD operations: Create, Read, Update, Delete dla projektów i zadań
  • UI: Tailwind CSS + shadcn/ui
  • Deployment: Podstawy wdrożenia na produkcję

To będzie MEGA obszerny wpis z pełnym kodem działającej aplikacji! Po tym wpisie będziesz miał gotowy template (szablon – podstawę) do budowania własnych projektów Full Stack.

Architektura projektu

🔍 Architektura Full Stack - jak to działa razem?

Podział odpowiedzialności:

┌─────────────────────────────────────────┐
│           UŻYTKOWNIK                    │
│   (przeglądarka: Chrome, Firefox)       │
└──────────────┬──────────────────────────┘
               │
               │ HTTP/HTTPS
               ↓
┌──────────────────────────────────────────┐
│          FRONTEND (React)                │
│  - UI/UX (co widzi użytkownik)           │
│  - Walidacja formularzy                  │
│  - Routing (nawigacja)                   │
│  - State management (Zustand, React Query)│
│  - Komunikacja z API (Axios)             │
└──────────────┬───────────────────────────┘
               │
               │ REST API (JSON)
               ↓
┌──────────────────────────────────────────┐
│          BACKEND (.NET)                  │
│  - Logika biznesowa (Services)           │
│  - Autentykacja (JWT)                    │
│  - Walidacja danych                      │
│  - CRUD operations                       │
└──────────────┬───────────────────────────┘
               │
               │ SQL
               ↓
┌──────────────────────────────────────────┐
│       BAZA DANYCH (SQL Server)           │
│  - Trwałe przechowywanie danych          │
│  - Relacje (Users ← Projects ← Tasks)    │
│  - Transakcje                            │
└──────────────────────────────────────────┘

Struktura folderów

🔍 Dlaczego taka struktura folderów?

Backend - Clean Architecture:

  • Controllers - przyjmują requesty HTTP, zwracają responses
  • Services - logika biznesowa (NIE w controllerach!)
  • Data - DbContext, konfiguracja bazy danych
  • Models - encje bazodanowe (User, Project, Task)
  • DTOs - obiekty do komunikacji (frontend ↔ backend)

Frontend - Feature-based:

  • api/ - Axios client, wszystkie API calls
  • components/ - reużywalne komponenty (Button, Modal)
  • pages/ - strony aplikacji (Dashboard, Login)
  • contexts/ - Context API (AuthContext)
  • stores/ - Zustand stores (globalny stan)
  • hooks/ - custom hooks (useAuth, useProjects)
task-manager/
├── backend/                    # .NET Core API
│   ├── TaskManager.API/
│   │   ├── Controllers/       # HTTP endpoints (API routes)
│   │   ├── Models/           # Database entities
│   │   ├── DTOs/             # Data Transfer Objects
│   │   ├── Services/         # Business logic
│   │   ├── Data/             # DbContext, migrations
│   │   ├── Middleware/       # Custom middleware
│   │   └── Program.cs        # App configuration
│   └── TaskManager.sln
│
├── frontend/                   # React + TypeScript
│   ├── src/
│   │   ├── api/              # Axios client, API calls
│   │   ├── components/       # Reusable components
│   │   ├── pages/            # Route pages
│   │   ├── contexts/         # AuthContext
│   │   ├── stores/           # Zustand stores
│   │   ├── hooks/            # Custom hooks
│   │   ├── types/            # TypeScript types
│   │   ├── lib/              # Utils, helpers
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── package.json
│   └── vite.config.ts
│
└── README.md

Stos technologiczny

🔍 Dlaczego wybraliśmy te technologie?

Backend - .NET Core:

  • ✅ Bardzo wydajne (top 10 frameworków na świecie!)
  • ✅ Entity Framework = łatwe zarządzanie bazą (ORM)
  • ✅ Silnie typowane (jak TypeScript)
  • ✅ Świetne narzędzia (Visual Studio, Rider)
  • ✅ Cross-platform (Windows, Linux, Mac)

Frontend - React + TypeScript:

  • ✅ TypeScript = zero błędów runtime (catch at compile time!)
  • ✅ Zustand = prosty global state (lżejszy niż Redux)
  • ✅ React Query = cache, refetch, mutations (idealny do API!)
  • ✅ Tailwind = szybkie stylowanie (utility-first CSS)
  • ✅ Vite = ultra szybki build (HMR w <50ms!)

Backend:

  • .NET 8.0 Web API
  • Entity Framework Core (ORM – Object-Relational Mapping, mapowanie obiektów na bazę)
  • SQL Server / PostgreSQL
  • JWT Authentication
  • AutoMapper (mapowanie DTOs)

Frontend:

  • React 18
  • TypeScript
  • Vite (build tool – narzędzie do budowania)
  • Zustand (zarządzanie stanem)
  • React Query (server state)
  • React Router (routing)
  • Tailwind CSS + shadcn/ui (UI components)
  • Axios (HTTP client)
📊 Data Flow - jak dane przepływają przez aplikację:
1. UŻYTKOWNIK KLIKA "Utwórz zadanie"
   ↓
2. FRONTEND (React)
   → Walidacja formularza (React Hook Form)
   → Optimistic update (pokazuje zadanie od razu!)
   → useMutation: POST /api/tasks
   ↓
3. AXIOS INTERCEPTOR
   → Dodaje Authorization: Bearer 
   → Wysyła JSON do backendu
   ↓
4. BACKEND (.NET) - TasksController
   → [Authorize] - sprawdza JWT token
   → Waliduje DTO (CreateTaskDto)
   → Wywołuje TaskService.CreateTaskAsync()
   ↓
5. TASK SERVICE
   → Sprawdza czy projekt istnieje
   → Tworzy encję Task
   → Zapisuje do DbContext
   → Zwraca TaskDto
   ↓
6. ENTITY FRAMEWORK
   → Generuje SQL: INSERT INTO Tasks (...)
   → Wykonuje na bazie danych
   → Zwraca ID nowego zadania
   ↓
7. BACKEND RESPONSE
   → 201 Created
   → Body: { id: 123, title: "...", ... }
   ↓
8. FRONTEND (React Query)
   → onSuccess: invalidateQueries(['tasks'])
   → Refetch wszystkich zadań
   → UI auto-updatuje się!
   ↓
9. UŻYTKOWNIK WIDZI NOWE ZADANIE ✅
Backend - .NET Core Web API

1. Setup projektu

🔍 Co instalujemy i dlaczego?
  • Microsoft.EntityFrameworkCore.SqlServer - ORM do SQL Server
  • Microsoft.EntityFrameworkCore.Design - narzędzia do migrations
  • Microsoft.AspNetCore.Authentication.JwtBearer - autentykacja JWT
  • AutoMapper - mapowanie Models → DTOs (mniej kodu!)
  • BCrypt.Net-Next - hashowanie haseł (NIGDY plain text!)
# Utwórz solution
dotnet new sln -n TaskManager

# Utwórz Web API projekt
dotnet new webapi -n TaskManager.API

# Dodaj projekt do solution
dotnet sln add TaskManager.API/TaskManager.API.csproj

# Zainstaluj NuGet packages
cd TaskManager.API
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package BCrypt.Net-Next

2. Models (modele danych)

🔍 Models vs DTOs - jaka różnica?

Models (encje bazodanowe):

  • Reprezentują TABELE w bazie danych
  • Mają relacje (Foreign Keys, Navigation Properties)
  • Entity Framework mapuje je na SQL
  • Przykład: User ma PasswordHash (NIE wysyłamy do frontendu!)

DTOs (Data Transfer Objects):

  • Reprezentują DANE wysyłane przez API
  • BEZ wrażliwych danych (hasła, tokeny)
  • BEZ relacji (płaska struktura)
  • Przykład: UserDto ma tylko Id, Name, Email, Role

Dlaczego rozdzielamy?

  • 🔒 Bezpieczeństwo (nie wysyłamy PasswordHash!)
  • 📦 Performance (mniejsze payloads)
  • 🛡️ Enkapsulacja (zmiany w Models nie łamią API)
// Models/User.cs
namespace TaskManager.API.Models;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string PasswordHash { get; set; } = string.Empty;  // ⚠️ NIGDY nie wysyłaj tego!
    public string Role { get; set; } = "user"; // user, admin
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    
    // Navigation properties (właściwości nawigacyjne - relacje)
    public ICollection Projects { get; set; } = new List();
    public ICollection Tasks { get; set; } = new List();
}
🔍 Navigation Properties - co to?

Navigation Properties = relacje między tabelami

// User ma wiele Projektów:
public ICollection Projects { get; set; }

// Entity Framework rozumie:
// - One-to-Many (1 User → wiele Projects)
// - Tworzy Foreign Key: Project.UserId → User.Id

// Możesz potem zrobić:
var user = await _context.Users
    .Include(u => u.Projects)  // Załaduj projekty
    .FirstAsync();

Console.WriteLine(user.Projects.Count);  // Ile projektów ma user?
// Models/Project.cs
namespace TaskManager.API.Models;

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string Color { get; set; } = "#3B82F6"; // Hex color
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? UpdatedAt { get; set; }
    
    // Foreign keys (klucze obce)
    public int UserId { get; set; }
    public User User { get; set; } = null!;
    
    // Navigation
    public ICollection Tasks { get; set; } = new List();
}
// Models/Task.cs
namespace TaskManager.API.Models;

public class Task
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public TaskStatus Status { get; set; } = TaskStatus.Todo;
    public TaskPriority Priority { get; set; } = TaskPriority.Medium;
    public DateTime? DueDate { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? UpdatedAt { get; set; }
    public DateTime? CompletedAt { get; set; }
    
    // Foreign keys
    public int ProjectId { get; set; }
    public Project Project { get; set; } = null!;
    
    public int UserId { get; set; }
    public User User { get; set; } = null!;
}

public enum TaskStatus
{
    Todo,
    InProgress,
    Done
}

public enum TaskPriority
{
    Low,
    Medium,
    High
}
🌟 Przykład: Relacje w bazie danych
// W bazie danych mamy:

┌──────────────┐
│    Users     │
├──────────────┤
│ Id   │ Name  │
├──────────────┤
│ 1    │ Jan   │
│ 2    │ Anna  │
└──────────────┘
       │
       │ 1:N (One-to-Many)
       ↓
┌───────────────────────┐
│      Projects         │
├───────────────────────┤
│ Id │ Name    │ UserId │  ← Foreign Key
├───────────────────────┤
│ 10 │ "Blog"  │   1    │  ← Jan's project
│ 11 │ "Shop"  │   1    │  ← Jan's project
│ 12 │ "App"   │   2    │  ← Anna's project
└───────────────────────┘
       │
       │ 1:N
       ↓
┌──────────────────────────────────┐
│           Tasks                  │
├──────────────────────────────────┤
│ Id │ Title      │ ProjectId │ UserId │
├──────────────────────────────────┤
│ 100│ "Design"   │    10     │   1    │
│ 101│ "Code"     │    10     │   1    │
│ 102│ "Deploy"   │    11     │   1    │
└──────────────────────────────────┘

// C# code:
var jan = await _context.Users
    .Include(u => u.Projects)  // Załaduj projekty Jana
        .ThenInclude(p => p.Tasks)  // I zadania w każdym projekcie
    .FirstAsync(u => u.Id == 1);

// jan.Projects[0].Name == "Blog"
// jan.Projects[0].Tasks.Count == 2 (Design, Code)

3. DTOs (Data Transfer Objects - obiekty transferu danych)

🔍 Record vs Class - dlaczego record?

Record (C# 9.0+):

  • ✅ Immutable (niemutowalne - bezpieczne)
  • ✅ Value equality (porównuje wartości, nie referencje)
  • ✅ Shorter syntax (mniej kodu!)
  • ✅ Idealne dla DTOs (dane tylko do odczytu)
// Record (KRÓTKO):
public record LoginDto(string Email, string Password);

// Odpowiednik Class (DŁUGO):
public class LoginDto {
    public string Email { get; init; }
    public string Password { get; init; }
    public LoginDto(string email, string password) {
        Email = email;
        Password = password;
    }
}
// DTOs/Auth/AuthDtos.cs
namespace TaskManager.API.DTOs.Auth;

public record LoginDto(string Email, string Password);

public record RegisterDto(
    string Name,
    string Email,
    string Password
);

public record AuthResponseDto(
    string AccessToken,
    UserDto User
);

public record UserDto(
    int Id,
    string Name,
    string Email,
    string Role
);  // ⚠️ Brak PasswordHash - bezpieczne!
// DTOs/Projects/ProjectDtos.cs
namespace TaskManager.API.DTOs.Projects;

public record CreateProjectDto(
    string Name,
    string Description,
    string Color
);

public record UpdateProjectDto(
    string Name,
    string Description,
    string Color
);

public record ProjectDto(
    int Id,
    string Name,
    string Description,
    string Color,
    DateTime CreatedAt,
    DateTime? UpdatedAt,
    int TaskCount,          // ← Computed field (liczba zadań)
    int CompletedTaskCount  // ← Computed field (ukończone zadania)
);
🔍 Computed fields w DTOs - po co?

Problem: Frontend potrzebuje statystyk (TaskCount, CompletedTaskCount)

Złe rozwiązanie:

// Frontend robi 2 dodatkowe requesty:
GET /api/projects/123
GET /api/projects/123/tasks  // Policz wszystkie
GET /api/projects/123/tasks?status=done  // Policz ukończone

// 3 requesty = wolno! ❌

Dobre rozwiązanie:

// Backend liczy w SQL:
SELECT 
    p.*,
    COUNT(t.Id) as TaskCount,
    COUNT(CASE WHEN t.Status = 'Done' THEN 1 END) as CompletedTaskCount
FROM Projects p
LEFT JOIN Tasks t ON p.Id = t.ProjectId
GROUP BY p.Id

// 1 request z wszystkim! ✅

4. DbContext - konfiguracja bazy danych

🔍 DbContext - co to?

DbContext = "most" między C# a bazą danych

  • DbSet<T> = tabela w bazie danych
  • Tracking changes (śledzi zmiany obiektów)
  • Generuje SQL automatycznie
  • Obsługuje transakcje
// C# Code:
var user = new User { Name = "Jan", Email = "jan@test.com" };
_context.Users.Add(user);
await _context.SaveChangesAsync();

// EF generuje SQL:
INSERT INTO Users (Name, Email, CreatedAt)
VALUES ('Jan', 'jan@test.com', '2024-01-01 10:00:00')
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using TaskManager.API.Models;

namespace TaskManager.API.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions options)
        : base(options)
    {
    }

    // DbSets = tabele w bazie danych
    public DbSet Users { get; set; } = null!;
    public DbSet Projects { get; set; } = null!;
    public DbSet Tasks { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Konfiguracja relacji
        modelBuilder.Entity()
            .HasOne(p => p.User)           // Project ma jednego User
            .WithMany(u => u.Projects)     // User ma wiele Projects
            .HasForeignKey(p => p.UserId)  // Foreign key: UserId
            .OnDelete(DeleteBehavior.Cascade);  // Usuń projekty gdy usuniesz usera

        modelBuilder.Entity()
            .HasOne(t => t.Project)
            .WithMany(p => p.Tasks)
            .HasForeignKey(t => t.ProjectId)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity()
            .HasOne(t => t.User)
            .WithMany(u => u.Tasks)
            .HasForeignKey(t => t.UserId)
            .OnDelete(DeleteBehavior.Restrict);  // NIE usuwaj zadań gdy usuniesz usera
    }
}
🔍 DeleteBehavior - Cascade vs Restrict:

Cascade (kaskadowe usuwanie):

// Usuwasz projekt:
DELETE FROM Projects WHERE Id = 10

// EF automatycznie usuwa WSZYSTKIE zadania tego projektu:
DELETE FROM Tasks WHERE ProjectId = 10

// Przydatne gdy: dane zależne tracą sens bez parenta

Restrict (blokada usuwania):

// Próbujesz usunąć użytkownika który ma zadania:
DELETE FROM Users WHERE Id = 1

// EF rzuca błąd:
// "Cannot delete User - has dependent Tasks"

// Musisz najpierw usunąć/przenieść zadania!

// Przydatne gdy: chcesz zachować historię
💪 Ćwiczenie 1: Zaprojektuj Model

Rozszerz Task Manager o nową encję Comment (komentarz do zadania):

  1. Pola: Id, Text, CreatedAt, TaskId, UserId
  2. Relacja: Task ma wiele Comments (1:N)
  3. Relacja: User ma wiele Comments (1:N)
  4. DeleteBehavior: Cascade dla Task, Restrict dla User
  5. Dodaj do DbContext
  6. Stwórz DTO: CommentDto, CreateCommentDto

Bonus: Dodaj pole UpdatedAt i IsEdited!

Services - logika biznesowa

🔍 Services Pattern - dlaczego oddzielamy logikę?

Bez Services (ZŁE):

// AuthController:
[HttpPost("register")]
public async Task Register(RegisterDto dto) {
    // Walidacja
    if (await _context.Users.AnyAsync(u => u.Email == dto.Email))
        return BadRequest("Email exists");
    
    // Hash hasła
    var hash = BCrypt.HashPassword(dto.Password);
    
    // Tworzenie usera
    var user = new User { ... };
    _context.Users.Add(user);
    await _context.SaveChangesAsync();
    
    // Generowanie tokenu
    var token = GenerateJwt(user);
    
    return Ok(new { token, user });
}
// 30 linii kodu w controllerze! ❌

Z Services (DOBRE):

// AuthController:
[HttpPost("register")]
public async Task Register(RegisterDto dto) {
    var result = await _authService.RegisterAsync(dto);
    return result == null 
        ? BadRequest("Email exists") 
        : Ok(result);
}
// 5 linii kodu! ✅

// Logika w AuthService (reużywalna, testowalna)

Zalety Services:

  • ✅ Separation of Concerns (rozdzielenie odpowiedzialności)
  • ✅ Reusability (reużywalność - możesz użyć w wielu controllerach)
  • ✅ Testability (łatwe testowanie bez HTTP)
  • ✅ Single Responsibility (jeden Service = jedna odpowiedzialność)

5. AuthService

🔍 Interface IAuthService - po co?

Dependency Injection + Interface = elastyczność i testowanie

// Interface (kontrakt):
public interface IAuthService {
    Task RegisterAsync(RegisterDto dto);
    Task LoginAsync(LoginDto dto);
}

// Implementacja:
public class AuthService : IAuthService {
    // ... implementacja
}

// Controller zależy od INTERFEJSU, nie implementacji:
public class AuthController {
    private readonly IAuthService _authService;  // ← Interface!
    
    public AuthController(IAuthService authService) {
        _authService = authService;
    }
}

// Zalety:
// 1. Testowanie: możesz podmienić na MockAuthService
// 2. Elastyczność: możesz zmienić implementację bez zmiany Controllera
// 3. SOLID: Dependency Inversion Principle
// Services/AuthService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using TaskManager.API.Data;
using TaskManager.API.DTOs.Auth;
using TaskManager.API.Models;

namespace TaskManager.API.Services;

public interface IAuthService
{
    Task RegisterAsync(RegisterDto dto);
    Task LoginAsync(LoginDto dto);
    Task GetCurrentUserAsync(int userId);
}

public class AuthService : IAuthService
{
    private readonly AppDbContext _context;
    private readonly IConfiguration _configuration;

    public AuthService(AppDbContext context, IConfiguration configuration)
    {
        _context = context;
        _configuration = configuration;
    }

    public async Task RegisterAsync(RegisterDto dto)
    {
        // Sprawdź czy użytkownik już istnieje
        if (await _context.Users.AnyAsync(u => u.Email == dto.Email))
        {
            throw new InvalidOperationException("Email już istnieje");
        }

        // Hash hasła (NIGDY nie zapisuj plain text!)
        var passwordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password);

        var user = new User
        {
            Name = dto.Name,
            Email = dto.Email,
            PasswordHash = passwordHash,
            Role = "user"
        };

        _context.Users.Add(user);
        await _context.SaveChangesAsync();

        var token = GenerateJwtToken(user);
        return new AuthResponseDto(
            token,
            new UserDto(user.Id, user.Name, user.Email, user.Role)
        );
    }

    public async Task LoginAsync(LoginDto dto)
    {
        var user = await _context.Users.FirstOrDefaultAsync(u => u.Email == dto.Email);
        
        // Sprawdź hasło (BCrypt.Verify porównuje hash)
        if (user == null || !BCrypt.Net.BCrypt.Verify(dto.Password, user.PasswordHash))
        {
            throw new UnauthorizedAccessException("Nieprawidłowy email lub hasło");
        }

        var token = GenerateJwtToken(user);
        return new AuthResponseDto(
            token,
            new UserDto(user.Id, user.Name, user.Email, user.Role)
        );
    }

    public async Task GetCurrentUserAsync(int userId)
    {
        var user = await _context.Users.FindAsync(userId);
        return user == null ? null : new UserDto(user.Id, user.Name, user.Email, user.Role);
    }

    private string GenerateJwtToken(User user)
    {
        // Claims = informacje w tokenie
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Name),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Role)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
            _configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT key not configured")));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddDays(7),  // Token wygasa za 7 dni
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}
🔍 BCrypt – dlaczego nie SHA256?

Problem z SHA256 (przy hasłach):

// SHA256 (NIE DO HASEŁ):
var hash = SHA256.HashData(Encoding.UTF8.GetBytes("password123"));
// hash = "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

// Problem:
// - deterministyczny (to samo hasło = ten sam hash)
// - ekstremalnie szybki (idealny do brute-force na GPU)
// - brak wbudowanego salta i kosztu obliczeniowego
// Rainbow tables i masowe ataki są realnym zagrożeniem
// ❌ NIEZALECANE DO PRZECHOWYWANIA HASEŁ

BCrypt (DOBRE):

// BCrypt:
var hash1 = BCrypt.HashPassword("password123");
// hash1 = "$2a$11$N9qo8uLOickgx2ZMRZoMy.L5Qf3Y8F..."

var hash2 = BCrypt.HashPassword("password123");
// hash2 = "$2a$11$VkJF8b5YhDJgSDFG23L43O.AH6SDG..."

// RÓŻNE HASHE dla tego samego hasła (wbudowany salt)
// + regulowany work factor (celowe spowolnienie)
// + znacznie wyższy koszt brute-force
// ✅ FUNKCJA ZAPROJEKTOWANA DO HASEŁ

Po co kolejne Services? ProjectService i TaskService mają podobną strukturę - spójrz do kodu źródłowego w repozytorium. Tutaj skupmy się na kluczowych konceptach!

🌟 Przykład: ProjectService - metoda GetAllProjects z computed fields
public async Task> GetAllProjectsAsync(int userId)
{
    return await _context.Projects
        .Where(p => p.UserId == userId)  // Tylko projekty tego usera
        .Include(p => p.Tasks)           // Załaduj zadania (JOIN)
        .Select(p => new ProjectDto(
            p.Id,
            p.Name,
            p.Description,
            p.Color,
            p.CreatedAt,
            p.UpdatedAt,
            p.Tasks.Count,  // ← Computed: liczba wszystkich zadań
            p.Tasks.Count(t => t.Status == TaskStatus.Done)  // ← Computed: ukończone
        ))
        .ToListAsync();
}

// Generuje SQL:
SELECT
    p.Id, p.Name, p.Description, p.Color, p.CreatedAt, p.UpdatedAt,
    COUNT(t.Id) as TaskCount,
    COUNT(CASE WHEN t.Status = 2 THEN 1 END) as CompletedTaskCount
FROM Projects p
LEFT JOIN Tasks t ON p.Id = t.ProjectId
WHERE p.UserId = @userId
GROUP BY p.Id, p.Name, p.Description, p.Color, p.CreatedAt, p.UpdatedAt

// JEDEN query zamiast N+1! ✅
⚠️ N+1 Problem - częsty błąd wydajnościowy!

Zły kod (N+1):

// 1. Pobierz projekty:
var projects = await _context.Projects.ToListAsync();  // 1 query

// 2. Dla KAŻDEGO projektu pobierz zadania:
foreach (var project in projects) {
    var taskCount = await _context.Tasks
        .Where(t => t.ProjectId == project.Id)
        .CountAsync();  // N queries (10 projektów = 10 queries!)
}

// Łącznie: 1 + N queries = 11 queries dla 10 projektów! ❌

Dobry kod (Include + Select):

var projects = await _context.Projects
    .Include(p => p.Tasks)  // JOIN w SQL
    .Select(p => new ProjectDto(
        // ...
        p.Tasks.Count  // Policzone w SQL, nie w C#
    ))
    .ToListAsync();

// Łącznie: 1 query! ✅
Podsumowanie części 1 - Backend

W pierwszej części stworzyliśmy kompletny backend z:

  • Models - encje bazodanowe (User, Project, Task)
  • DTOs - bezpieczna komunikacja z frontendem
  • DbContext - konfiguracja bazy + relacje
  • Services - logika biznesowa (Auth, Project, Task)
  • Best Practices - BCrypt, Interfaces, Computed fields, N+1 avoidance

W części 2 zobaczymy:

  • Controllers (REST API endpoints)
  • Program.cs (konfiguracja, JWT, CORS)
  • Frontend React setup
  • Komunikacja Frontend ↔ Backend
Controllers - REST API Endpoints

🔍 Controllers - co to i jak działają?

Controller = "recepcjonista API" - przyjmuje requesty HTTP, zwraca responses

┌─────────────────────────────────────────────┐
│   Frontend wysyła request:                  │
│   POST /api/auth/login                      │
│   Body: { email: "...", password: "..." }   │
└──────────────┬──────────────────────────────┘
               │
               ↓
┌──────────────────────────────────────────────┐
│   AuthController (endpoint)                  │
│   - Sprawdza routing ([Route("api/[controller]")])│
│   - Deserializuje JSON → LoginDto            │
│   - Wywołuje _authService.LoginAsync(dto)    │
│   - Zwraca response: Ok(result) lub Unauthorized()│
└──────────────┬───────────────────────────────┘
               │
               ↓
┌──────────────────────────────────────────────┐
│   Response JSON:                             │
│   200 OK                                     │
│   { "accessToken": "...", "user": {...} }    │
└──────────────────────────────────────────────┘

Attributes (adnotacje):

  • [ApiController] - automatyczna walidacja, model binding
  • [Route("api/[controller]")] - bazowa ścieżka (np. /api/auth)
  • [HttpPost("login")] - HTTP metoda + sub-path (/api/auth/login)
  • [Authorize] - wymaga JWT tokenu (chronione!)

6. AuthController

🔍 HTTP Status Codes - które zwracać?

Success (2xx):

  • 200 OK - sukces (GET, PUT)
  • 201 Created - zasób utworzony (POST) + Location header
  • 204 No Content - sukces bez body (DELETE)

Client Error (4xx):

  • 400 Bad Request - błędne dane (walidacja)
  • 401 Unauthorized - brak/nieprawidłowy token
  • 403 Forbidden - token OK, ale brak uprawnień
  • 404 Not Found - zasób nie istnieje

Server Error (5xx):

  • 500 Internal Server Error - nieoczekiwany błąd
// Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using TaskManager.API.DTOs.Auth;
using TaskManager.API.Services;

namespace TaskManager.API.Controllers;

[ApiController]  // ← Automatyczna walidacja, problem details
[Route("api/[controller]")]  // ← Route: /api/auth
public class AuthController : ControllerBase
{
    private readonly IAuthService _authService;

    public AuthController(IAuthService authService)
    {
        _authService = authService;  // Dependency Injection
    }

    [HttpPost("register")]  // ← POST /api/auth/register
    public async Task> Register(RegisterDto dto)
    {
        try
        {
            var response = await _authService.RegisterAsync(dto);
            return Ok(response);  // ← 200 OK + JSON body
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(new { message = ex.Message });  // ← 400 Bad Request
        }
    }

    [HttpPost("login")]  // ← POST /api/auth/login
    public async Task> Login(LoginDto dto)
    {
        try
        {
            var response = await _authService.LoginAsync(dto);
            return Ok(response);  // ← 200 OK
        }
        catch (UnauthorizedAccessException ex)
        {
            return Unauthorized(new { message = ex.Message });  // ← 401 Unauthorized
        }
    }

    [HttpPost("logout")]  // ← POST /api/auth/logout
    public IActionResult Logout()
    {
        return Ok(new { message = "Wylogowano pomyślnie" });
    }
}
🌟 Przykład: Request/Response flow w AuthController
// Frontend request:
POST http://localhost:5000/api/auth/login
Content-Type: application/json

{
  "email": "jan@test.com",
  "password": "haslo123"
}

// Controller deserializuje JSON → LoginDto
// Wywołuje _authService.LoginAsync(dto)
// Service zwraca AuthResponseDto

// Backend response:
HTTP/1.1 200 OK
Content-Type: application/json

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 1,
    "name": "Jan Kowalski",
    "email": "jan@test.com",
    "role": "user"
  }
}

// Frontend zapisuje token + user w state!
// Użytkownik zalogowany! ✅

7. ProjectsController

🔍 [Authorize] - jak działa autoryzacja?
// [Authorize] na klasie = WSZYSTKIE metody wymagają tokenu

[Authorize]  // ← Każdy endpoint w tym kontrollerze chroniony!
public class ProjectsController : ControllerBase
{
    [HttpGet]  // GET /api/projects - wymaga tokenu
    [HttpPost] // POST /api/projects - wymaga tokenu
    [HttpDelete("{id}")] // DELETE /api/projects/123 - wymaga tokenu
}

// Flow autoryzacji:
1. Frontend wysyła: Authorization: Bearer 
2. JWT Middleware sprawdza token
3. Jeśli token OK → User.Claims zawiera dane z tokenu
4. GetUserId() wyciąga User ID z Claims
5. Service używa userId → użytkownik widzi TYLKO swoje dane!

// Bez tokenu:
GET /api/projects
→ 401 Unauthorized ❌

// Z tokenem:
GET /api/projects
Authorization: Bearer eyJhbGci...
→ 200 OK + projekty tego użytkownika ✅
// Controllers/ProjectsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using TaskManager.API.DTOs.Projects;
using TaskManager.API.Services;

namespace TaskManager.API.Controllers;

[Authorize]  // ← Wszystkie endpointy wymagają JWT tokenu
[ApiController]
[Route("api/[controller]")]  // ← /api/projects
public class ProjectsController : ControllerBase
{
    private readonly IProjectService _projectService;

    public ProjectsController(IProjectService projectService)
    {
        _projectService = projectService;
    }

    // Helper - wyciąga User ID z JWT tokenu
    private int GetUserId()
    {
        var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        return int.Parse(userIdClaim!);
    }

    [HttpGet]  // ← GET /api/projects
    public async Task>> GetAll()
    {
        var projects = await _projectService.GetAllProjectsAsync(GetUserId());
        return Ok(projects);  // ← 200 OK + lista projektów
    }

    [HttpGet("{id}")]  // ← GET /api/projects/123
    public async Task> GetById(int id)
    {
        var project = await _projectService.GetProjectByIdAsync(id, GetUserId());

        if (project == null)
            return NotFound();  // ← 404 Not Found

        return Ok(project);
    }

    [HttpPost]  // ← POST /api/projects
    public async Task> Create(CreateProjectDto dto)
    {
        var project = await _projectService.CreateProjectAsync(dto, GetUserId());
        return CreatedAtAction(nameof(GetById), new { id = project.Id }, project);
        // ← 201 Created + Location: /api/projects/123
    }

    [HttpPut("{id}")]  // ← PUT /api/projects/123
    public async Task> Update(int id, UpdateProjectDto dto)
    {
        try
        {
            var project = await _projectService.UpdateProjectAsync(id, dto, GetUserId());
            return Ok(project);
        }
        catch (InvalidOperationException)
        {
            return NotFound();
        }
    }

    [HttpDelete("{id}")]  // ← DELETE /api/projects/123
    public async Task Delete(int id)
    {
        try
        {
            await _projectService.DeleteProjectAsync(id, GetUserId());
            return NoContent();  // ← 204 No Content (sukces, brak body)
        }
        catch (InvalidOperationException)
        {
            return NotFound();
        }
    }
}
🔍 RESTful API Design - konwencje:
HTTP Endpoint Operacja Response
GET /api/projects Pobierz wszystkie 200 + lista
GET /api/projects/123 Pobierz jeden 200 + obiekt
POST /api/projects Utwórz nowy 201 + Location header
PUT /api/projects/123 Aktualizuj 200 + zaktualizowany
DELETE /api/projects/123 Usuń 204 (no content)

Złote zasady REST:

  • ✅ Rzeczowniki w URL (/projects, nie /getProjects)
  • ✅ HTTP methods określają operację (GET, POST, PUT, DELETE)
  • ✅ Stateless (każdy request niezależny)
  • ✅ Hierarchia zasobów (/projects/123/tasks)

TasksController ma identyczną strukturę - zobacz kod źródłowy!

9. Program.cs - konfiguracja aplikacji

🔍 Program.cs - co tu się dzieje?

Program.cs = miejsce gdzie konfigurujemy CAŁĄ aplikację

  1. builder.Services.Add... - rejestracja serwisów (Dependency Injection)
  2. builder.Build() - budowanie aplikacji
  3. app.Use... - middleware pipeline (kolejność WAŻNA!)
  4. app.Run() - uruchamia serwer
// Program.cs
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using TaskManager.API.Data;
using TaskManager.API.Services;

var builder = WebApplication.CreateBuilder(args);

// === DEPENDENCY INJECTION === 

// Database - DbContext jako Scoped (jeden per request)
builder.Services.AddDbContext(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Services - Scoped (nowa instancja per request)
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();

// JWT Authentication
var jwtKey = builder.Configuration["Jwt:Key"]!;
var jwtIssuer = builder.Configuration["Jwt:Issuer"]!;
var jwtAudience = builder.Configuration["Jwt:Audience"]!;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,          // Sprawdź issuer
            ValidateAudience = true,        // Sprawdź audience
            ValidateLifetime = true,        // Sprawdź czy token nie wygasł
            ValidateIssuerSigningKey = true, // Sprawdź podpis
            ValidIssuer = jwtIssuer,
            ValidAudience = jwtAudience,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
        };
    });

// CORS - pozwól na requesty z frontendu
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:5173") // Vite default port
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();  // Dla cookies
    });
});

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();  // Swagger UI dla testowania API

var app = builder.Build();

// === MIDDLEWARE PIPELINE (KOLEJNOŚĆ WAŻNA!) ===

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();      // Swagger endpoint
    app.UseSwaggerUI();    // Swagger UI
}

app.UseCors("AllowFrontend");  // ← PRZED Authentication!
app.UseAuthentication();       // ← Sprawdza JWT token
app.UseAuthorization();        // ← Sprawdza uprawnienia
app.MapControllers();          // ← Routing do controllerów

app.Run();  // Uruchom serwer
🔍 Middleware Pipeline - kolejność ma znaczenie!
Request przechodzi przez middleware w tej kolejności:

1. UseCors()
   → Sprawdza origin (http://localhost:5173)
   → Dodaje CORS headers
   
2. UseAuthentication()
   → Czyta header: Authorization: Bearer 
   → Waliduje JWT token
   → Ustawia User.Claims (userId, email, role)
   
3. UseAuthorization()
   → Sprawdza [Authorize] attributes
   → Jeśli brak tokenu → 401 Unauthorized
   
4. MapControllers()
   → Routing do odpowiedniego controllera
   → Wywołuje metodę (np. GetAll())
   
5. Response z powrotem do klienta

// BŁĄD - zła kolejność:
app.UseAuthorization();  // ← Sprawdza token
app.UseAuthentication(); // ← Parsuje token (za późno!)
// Rezultat: WSZYSTKIE requesty 401! ❌
⚠️ CORS - co to i dlaczego potrzebujemy?

CORS (Cross-Origin Resource Sharing) = polityka bezpieczeństwa przeglądarek

// Scenariusz:
// Frontend: http://localhost:5173 (React)
// Backend:  http://localhost:5000 (API)

// RÓŻNE DOMENY/PORTY!

// Bez CORS:
fetch('http://localhost:5000/api/projects')
→ CORS error: "No 'Access-Control-Allow-Origin' header" ❌

// Z CORS:
builder.Services.AddCors(options => {
    options.AddPolicy("AllowFrontend", policy => {
        policy.WithOrigins("http://localhost:5173")  // Pozwól na requesty z Vite
    });
});

fetch('http://localhost:5000/api/projects')
→ 200 OK ✅

10. appsettings.json - konfiguracja

🔍 appsettings.json - sekretna konfiguracja

appsettings.json = ustawienia aplikacji (connection strings, JWT keys)

// Development (lokalnie):
appsettings.Development.json

// Production (serwer):
appsettings.Production.json

// ⚠️ NIGDY nie commituj prawdziwych JWT keys do Git!
// Używaj Environment Variables lub Azure Key Vault
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=TaskManagerDb;Trusted_Connection=true;TrustServerCertificate=true"
  },
  "Jwt": {
    "Key": "twoj-super-tajny-klucz-minimum-32-znaki-long",
    "Issuer": "TaskManagerAPI",
    "Audience": "TaskManagerClient"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

11. Migracje (migrations)

🔍 Entity Framework Migrations - jak działają?

Migration = "snapshot" schematu bazy danych

// 1. Tworzysz Models (User, Project, Task)
public class User {
    public int Id { get; set; }
    public string Name { get; set; }
}

// 2. Tworzysz migration (generuje kod C#):
dotnet ef migrations add InitialCreate

// EF generuje:
// - Migrations/20240101_InitialCreate.cs - kod migracji
// - Migrations/AppDbContextModelSnapshot.cs - snapshot modelu

// 3. Migration zawiera:
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateTable(
        name: "Users",
        columns: table => new {
            Id = table.Column(nullable: false)
                .Annotation("SqlServer:Identity", "1, 1"),
            Name = table.Column(nullable: false)
        });
}

// 4. Aplikujesz migration (tworzy tabele w DB):
dotnet ef database update

// EF wykonuje SQL:
CREATE TABLE Users (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(MAX) NOT NULL
);

Zalety migrations:

  • ✅ Version control dla bazy danych
  • ✅ Automatyczne tworzenie tabel
  • ✅ Rollback (cofnięcie zmian)
  • ✅ Team collaboration (wszyscy mają te same tabele)
# Utwórz migrację
dotnet ef migrations add InitialCreate

# Zaktualizuj bazę danych (wykonaj migration)
dotnet ef database update

# Usuń ostatnią migration (jeśli jeszcze nie applied)
dotnet ef migrations remove

# Uruchom API
dotnet run

# API działa na: http://localhost:5000
# Swagger UI: http://localhost:5000/swagger
💪 Ćwiczenie 2: Testowanie API w Swagger

Uruchom backend i przetestuj API:

  1. Uruchom: dotnet run
  2. Otwórz: http://localhost:5000/swagger
  3. POST /api/auth/register - zarejestruj użytkownika
  4. POST /api/auth/login - zaloguj się, skopiuj token
  5. Kliknij "Authorize" → wklej token
  6. POST /api/projects - utwórz projekt (wymaga tokenu!)
  7. GET /api/projects - pobierz projekty
  8. POST /api/tasks - utwórz zadanie

Sprawdź czy wszystko działa poprawnie!

Frontend - React + TypeScript Setup

1. Setup projektu

🔍 Vite - dlaczego nie Create React App?

Vite vs Create React App:

Create React App Vite
Start dev ~30s ~1s ⚡
HMR Wolne < 50ms ⚡
Build Webpack Rollup (szybszy)
Config Eject required vite.config.ts ✅

Vite = znacznie szybszy developer experience!

# Utwórz projekt Vite
npm create vite@latest frontend -- --template react-ts
cd frontend

# Zainstaluj zależności
npm install

# Zainstaluj dodatkowe pakiety
npm install react-router-dom          # Routing
npm install @tanstack/react-query     # Server state
npm install zustand                   # Global state
npm install axios                     # HTTP client
npm install date-fns                  # Daty
npm install lucide-react              # Ikony

# Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
🔍 Pakiety - po co każdy?
  • react-router-dom - routing (/login, /dashboard, /projects/:id)
  • @tanstack/react-query - cache, mutations, refetch (najlepsze do API!)
  • zustand - global state (auth, theme - prostszy niż Redux)
  • axios - HTTP client (interceptors, auto JSON)
  • date-fns - formatowanie dat (lżejsza niż moment.js)
  • lucide-react - ikony (SVG, tree-shakeable)
  • tailwindcss - utility-first CSS (szybkie stylowanie)

2. Tailwind config

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

3. TypeScript types

🔍 TypeScript types - dlaczego tak ważne?

Bez TypeScript:

// JavaScript - błąd w runtime:
const project = await fetchProject(123);
console.log(project.taskCount);  // undefined! (typo: TaskCount)
// Błąd dopiero gdy uruchomisz! ❌

Z TypeScript:

// TypeScript - błąd w edytorze:
const project: ProjectDto = await fetchProject(123);
console.log(project.taskCount);
//                  ^^^^^^^^^ Property 'taskCount' does not exist
// Czerwona kreska w VSCode! ✅

// Autocomplete działa! IDE podpowiada pola!
// src/types/index.ts

// User
export interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

// Auth
export interface LoginDto {
  email: string;
  password: string;
}

export interface RegisterDto {
  name: string;
  email: string;
  password: string;
}

export interface AuthResponse {
  accessToken: string;
  user: User;
}

// Project
export interface ProjectDto {
  id: number;
  name: string;
  description: string;
  color: string;
  createdAt: string;
  updatedAt: string | null;
  taskCount: number;
  completedTaskCount: number;
}

export interface CreateProjectDto {
  name: string;
  description: string;
  color: string;
}

// Task
export interface TaskDto {
  id: number;
  title: string;
  description: string;
  status: 'Todo' | 'InProgress' | 'Done';
  priority: 'Low' | 'Medium' | 'High';
  dueDate: string | null;
  createdAt: string;
  updatedAt: string | null;
  completedAt: string | null;
  projectId: number;
  projectName: string;
  projectColor: string;
}

export interface CreateTaskDto {
  title: string;
  description: string;
  projectId: number;
  status?: 'Todo' | 'InProgress' | 'Done';
  priority?: 'Low' | 'Medium' | 'High';
  dueDate?: string | null;
}
🌟 Przykład: TypeScript autocomplete w akcji
// Masz interface:
interface ProjectDto {
  id: number;
  name: string;
  taskCount: number;
}

// W komponencie:
const project: ProjectDto = await fetchProject(1);

// VSCode podpowiada:
project.  // ← Wciśnij Ctrl+Space
// Autocomplete pokazuje:
// - id: number
// - name: string
// - taskCount: number

// Jeśli napiszesz:
project.task  // ← Automatycznie uzupełni do "taskCount"

// Jeśli napiszesz błędnie:
project.tasks  // ← Czerwona kreska: "Property 'tasks' does not exist"

// To oszczędza GODZINY debugowania! ✅
Podsumowanie części 2 - Controllers + Frontend Setup

W drugiej części zbudowaliśmy:

  • Controllers - REST API endpoints (AuthController, ProjectsController, TasksController)
  • Program.cs - konfiguracja (JWT, CORS, Middleware Pipeline)
  • Migrations - tworzenie bazy danych
  • Frontend Setup - Vite + TypeScript + Tailwind + pakiety
  • TypeScript Types - type safety dla API responses

W części 3 (ostatniej) zobaczymy:

  • Axios client + Interceptors
  • AuthContext (Zustand)
  • React Query hooks
  • Komponenty UI
  • Integration frontend ↔ backend (pełny flow!)
  • Deployment na produkcję
  • Troubleshooting (typowe błędy)
Frontend Integration - łączenie z Backend

4. Axios Client + Interceptors

🔍 Axios vs Fetch - dlaczego Axios?

Fetch API (built-in):

// Fetch - dużo kodu:
const response = await fetch('http://localhost:5000/api/projects', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify(data)
});

if (!response.ok) throw new Error('Failed');
const result = await response.json();  // Trzeba ręcznie parsować!

Axios (lepsze):

// Axios - mniej kodu:
const { data } = await axios.post('http://localhost:5000/api/projects', data);
// Auto JSON parse! ✅
// Auto error handling! ✅
// Interceptors! ✅
// src/api/client.ts
import axios from 'axios';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api';

export const apiClient = axios.create({
  baseURL: API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request Interceptor - dodaj token do KAŻDEGO requesta
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response Interceptor - obsługa błędów (401 = wyloguj)
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token wygasł lub nieprawidłowy → wyloguj
      localStorage.removeItem('token');
      localStorage.removeItem('user');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);
🔍 Interceptors - automatyzacja!

Request Interceptor = "przed wysłaniem"

// Każdy request przechodzi przez interceptor:

// BEZ interceptora (duplikacja kodu):
axios.get('/projects', { 
  headers: { Authorization: `Bearer ${token}` } 
});
axios.post('/tasks', data, { 
  headers: { Authorization: `Bearer ${token}` } 
});
// Token w każdym request ręcznie! ❌

// Z interceptorem (automatycznie):
axios.get('/projects');  // ← Token dodany automatycznie!
axios.post('/tasks', data);  // ← Token dodany automatycznie!
// Interceptor dodaje token do KAŻDEGO requesta! ✅

Response Interceptor = "po otrzymaniu"

// Automatyczna obsługa 401:

// BEZ interceptora (duplikacja):
try {
  await axios.get('/projects');
} catch (error) {
  if (error.response?.status === 401) {
    logout();  // Trzeba w każdym catch! ❌
  }
}

// Z interceptorem (automatycznie):
await axios.get('/projects');
// Jeśli 401 → interceptor automatycznie wyloguje! ✅

5. API Functions - typy i funkcje

🔍 API Layer - dlaczego osobne funkcje?

Bez API layer (ZŁE):

// W komponencie:
const { data } = await axios.get('http://localhost:5000/api/projects');

// Problemy:
// ❌ URL hardcoded w wielu miejscach
// ❌ Brak reużywalności
// ❌ Trudne testowanie
// ❌ Zmiana API = zmiana wszędzie

Z API layer (DOBRE):

// src/api/projects.ts
export const projectsApi = {
  getAll: () => apiClient.get('/projects')
};

// W komponencie:
const { data } = await projectsApi.getAll();

// Zalety:
// ✅ URL w jednym miejscu
// ✅ Reużywalne
// ✅ Łatwe testowanie (mock projectsApi)
// ✅ Zmiana API = zmiana w jednym pliku
// src/api/auth.ts
import { apiClient } from './client';
import type { AuthResponse } from '../types';

export const authApi = {
  register: async (name: string, email: string, password: string): Promise => {
    const { data } = await apiClient.post('/auth/register', {
      name,
      email,
      password,
    });
    return data;
  },

  login: async (email: string, password: string): Promise => {
    const { data } = await apiClient.post('/auth/login', {
      email,
      password,
    });
    return data;
  },

  logout: async (): Promise => {
    await apiClient.post('/auth/logout');
  },
};
// src/api/projects.ts
import { apiClient } from './client';
import type { Project, CreateProjectDto, UpdateProjectDto } from '../types';

export const projectsApi = {
  getAll: async (): Promise => {
    const { data } = await apiClient.get('/projects');
    return data;
  },

  getById: async (id: number): Promise => {
    const { data } = await apiClient.get(`/projects/${id}`);
    return data;
  },

  create: async (dto: CreateProjectDto): Promise => {
    const { data } = await apiClient.post('/projects', dto);
    return data;
  },

  update: async (id: number, dto: UpdateProjectDto): Promise => {
    const { data } = await apiClient.put(`/projects/${id}`, dto);
    return data;
  },

  delete: async (id: number): Promise => {
    await apiClient.delete(`/projects/${id}`);
  },
};

tasksApi ma identyczną strukturę - zobacz kod źródłowy!

6. Zustand Store - zarządzanie stanem Auth

🔍 Zustand vs Context API - dlaczego Zustand?
Context API Zustand
Setup Provider + createContext create() - one liner
Boilerplate Dużo Mało ✅
Re-renders Wszystkie consumers Tylko używane ✅
DevTools Brak Tak ✅
Persist Ręcznie persist() middleware ✅
// src/stores/useAuthStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { User } from '../types';

interface AuthState {
  user: User | null;
  token: string | null;
  setAuth: (user: User, token: string) => void;
  clearAuth: () => void;
  isAuthenticated: boolean;
}

export const useAuthStore = create()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      setAuth: (user, token) => {
        localStorage.setItem('token', token);
        localStorage.setItem('user', JSON.stringify(user));
        set({ user, token, isAuthenticated: true });
      },

      clearAuth: () => {
        localStorage.removeItem('token');
        localStorage.removeItem('user');
        set({ user: null, token: null, isAuthenticated: false });
      },
    }),
    {
      name: 'auth-storage',  // LocalStorage key
      partialize: (state) => ({
        user: state.user,
        token: state.token,
        isAuthenticated: state.isAuthenticated,
      }),
    }
  )
);
🔍 Używanie Zustand w komponentach:
// W komponencie:
import { useAuthStore } from './stores/useAuthStore';

function Navbar() {
  // Wybierz tylko co potrzebujesz (selective subscription):
  const user = useAuthStore(state => state.user);
  const clearAuth = useAuthStore(state => state.clearAuth);
  
  // Komponent re-renderuje SIĘ TYLKO gdy user się zmieni!
  // Nie re-renderuje gdy token/isAuthenticated się zmieni! ✅
  
  return (
                
{user?.name}
); } // Alternatywnie - cały state (re-render gdy cokolwiek się zmieni): const { user, isAuthenticated, clearAuth } = useAuthStore();

7. React Query Setup + Custom Hooks

🔍 React Query - domyślna konfiguracja

Dlaczego te ustawienia?

  • retry: 1 - jedna ponowna próba (nie 3 jak domyślnie)
  • refetchOnWindowFocus: false - nie refetch gdy wrócisz do okna
  • staleTime: 5 * 60 * 1000 - dane świeże przez 5 minut
// Bez staleTime (domyślnie 0):
// Każdy re-mount komponentu = refetch
const { data } = useQuery({ queryKey: ['projects'] });
// Użytkownik przechodzi /dashboard → /profile → /dashboard
// = 2x fetch projects! ❌

// Ze staleTime: 5 minut:
// Dane świeże przez 5 minut = brak refetch
// Użytkownik przechodzi /dashboard → /profile → /dashboard
// = cache hit! Brak fechu! ✅
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from './App';
import './index.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,                       // Jedna ponowna próba
      refetchOnWindowFocus: false,    // Nie refetch przy focus
      staleTime: 5 * 60 * 1000,       // Dane świeże przez 5 minut
    },
  },
});

ReactDOM.createRoot(document.getElementById('root')!).render(
            
            
            
            
    
  
);

8. Custom Hooks - React Query

🔍 Custom Hooks Pattern - dlaczego?

Bez custom hooks (duplikacja):

// W Dashboard:
const { data: projects } = useQuery({
  queryKey: ['projects'],
  queryFn: projectsApi.getAll
});

// W ProjectsList:
const { data: projects } = useQuery({
  queryKey: ['projects'],  // Ta sama konfiguracja!
  queryFn: projectsApi.getAll
});
// Duplikacja! ❌

Z custom hooks (reużywalność):

// hooks/useProjects.ts
export function useProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: projectsApi.getAll
  });
}

// W Dashboard:
const { data: projects } = useProjects();

// W ProjectsList:
const { data: projects } = useProjects();
// Brak duplikacji! ✅
// src/hooks/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { projectsApi } from '../api/projects';
import { CreateProjectDto, UpdateProjectDto } from '../types';

// Query - pobieranie danych
export function useProjects() {
  return useQuery({
    queryKey: ['projects'],
    queryFn: projectsApi.getAll,
  });
}

export function useProject(id: number) {
  return useQuery({
    queryKey: ['projects', id],
    queryFn: () => projectsApi.getById(id),
    enabled: !!id,  // Fetch tylko gdy id istnieje
  });
}

// Mutations - modyfikacja danych
export function useCreateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateProjectDto) => projectsApi.create(data),
    onSuccess: () => {
      // Invalidate cache → auto refetch!
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });
}

export function useUpdateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: number; data: UpdateProjectDto }) =>
      projectsApi.update(id, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });
}

export function useDeleteProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: number) => projectsApi.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['projects'] });
    },
  });
}
🔍 invalidateQueries - dlaczego?
// Scenariusz:
// 1. Użytkownik widzi listę projektów (cache)
const { data: projects } = useProjects();
// projects = [Project1, Project2]

// 2. Tworzy nowy projekt
const createProject = useCreateProject();
await createProject.mutateAsync({ name: "New", ... });

// 3. Bez invalidation:
// Lista NADAL pokazuje [Project1, Project2] ❌
// Użytkownik: "Gdzie mój nowy projekt?" 😕

// 4. Z invalidation (onSuccess):
queryClient.invalidateQueries({ queryKey: ['projects'] });
// → React Query automatycznie refetchuje projekty
// → Lista pokazuje [Project1, Project2, New] ✅
// Użytkownik: "Wow, działa!" 🎉

useTasks ma identyczną strukturę - zobacz kod źródłowy!

🌟 Przykład: Użycie custom hooks w komponencie
// Dashboard.tsx
import { useProjects, useCreateProject } from './hooks/useProjects';

function Dashboard() {
  // Query - pobierz projekty
  const { data: projects, isLoading, error } = useProjects();
  
  // Mutation - utwórz projekt
  const createProject = useCreateProject();
  
  async function handleCreate() {
    await createProject.mutateAsync({
      name: "Nowy Projekt",
      description: "Opis",
      color: "#3B82F6"
    });
    // onSuccess auto-wywołuje invalidateQueries
    // → projects auto-refetch
    // → UI auto-update! ✅
  }
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
        <button onClick={handleCreate}>
            Utwórz projekt
        </button>
      {projects?.map(p => (
                <div key={p.id}>{p.name}</div>
      ))}
    </div>
  );
}
Full Stack Integration - kompletny flow

📊 Kompletny flow: Użytkownik tworzy zadanie
🏁 START - Użytkownik klika "Utwórz zadanie"

1. FRONTEND - Komponent Dashboard
   → Użytkownik wypełnia formularz
   → Klika "Save"
   → handleSubmit wywołuje: createTask.mutateAsync(data)

2. FRONTEND - React Query (useMutation)
   → mutationFn: tasksApi.create(data)
   → Pokazuje loading state

3. FRONTEND - API Layer
   → tasksApi.create() wywołuje apiClient.post('/tasks', data)

4. FRONTEND - Axios Interceptor (Request)
   → Dodaje header: Authorization: Bearer 
   → Token z localStorage

5. HTTP REQUEST
   → POST http://localhost:5000/api/tasks
   → Headers: { Authorization: "Bearer eyJ...", Content-Type: "application/json" }
   → Body: { title: "Fix bug", projectId: 1, status: "Todo", ... }

6. BACKEND - CORS Middleware
   → Sprawdza origin (http://localhost:5173)
   → Dodaje CORS headers
   → Allow ✅

7. BACKEND - Authentication Middleware
   → Czyta header Authorization
   → Waliduje JWT token
   → Ustawia User.Claims (userId: 123)

8. BACKEND - TasksController.Create()
   → [Authorize] sprawdzony ✅
   → Deserializuje JSON → CreateTaskDto
   → Wywołuje _taskService.CreateTaskAsync(dto, userId: 123)

9. BACKEND - TaskService
   → Sprawdza czy projekt istnieje
   → Tworzy encję Task { Title, ProjectId, UserId: 123, ... }
   → _context.Tasks.Add(task)
   → await _context.SaveChangesAsync()

10. DATABASE - Entity Framework
    → Generuje SQL: INSERT INTO Tasks (Title, ProjectId, UserId, ...) VALUES (...)
    → Wykonuje na SQL Server
    → Zwraca Id nowego zadania (id: 456)

11. BACKEND - Response
    → TaskService zwraca TaskDto
    → Controller: CreatedAtAction → 201 Created
    → Body: { id: 456, title: "Fix bug", status: "Todo", ... }

12. HTTP RESPONSE
    → 201 Created
    → Body: TaskDto JSON

13. FRONTEND - Axios Interceptor (Response)
    → Status 201 → OK, przepuść
    → return response

14. FRONTEND - React Query (onSuccess)
    → queryClient.invalidateQueries({ queryKey: ['tasks'] })
    → queryClient.invalidateQueries({ queryKey: ['projects'] })
    → Auto refetch tasks i projects!

15. FRONTEND - UI Update
    → useQuery(['tasks']) auto re-fetchuje
    → Nowe zadanie pojawia się na liście
    → useQuery(['projects']) auto re-fetchuje
    → taskCount zwiększony!

16. FRONTEND - Loading state OFF
    → isLoading: false
    → Formularz czysty
    → Success toast: "Zadanie utworzone!" ✅

🏁 KONIEC - Użytkownik widzi nowe zadanie na liście!

⏱️ Czas: ~500ms
🔄 Requesty: 3 (POST /tasks, GET /tasks, GET /projects)
✨ UX: Płynne, bez migotania, auto-update!
🔍 Kluczowe punkty integracji:

1. Type Safety (TypeScript):

  • Frontend CreateTaskDto = Backend CreateTaskDto (te same pola!)
  • IDE podpowiada pola podczas pisania
  • Błędy złapane PRZED uruchomieniem

2. Token Flow:

  • Login → token zapisany w localStorage
  • Każdy request → interceptor dodaje token
  • Backend → weryfikuje token, wyciąga userId
  • Service → używa userId (security!)

3. Cache Management:

  • React Query cache: ['tasks'], ['projects']
  • Po mutation → invalidate obu!
  • Auto refetch → UI auto update
  • Użytkownik nie widzi opóźnień!
Deployment - wdrożenie na produkcję

🔍 Deployment - opcje hostingu

Backend (.NET):

  • Azure App Service - Microsoft, łatwa integracja
  • Railway - prosty deployment, darmowy tier
  • Fly.io - szybki, globalny
  • Heroku - klasyk (płatny od 2022)

Frontend (React):

  • Vercel - NAJLEPSZY dla React (zero-config!)
  • Netlify - również świetny
  • GitHub Pages - darmowy, statyczny
  • Cloudflare Pages - super szybki CDN

Backend Deployment (Railway przykład)

# 1. Zainstaluj Railway CLI
npm install -g @railway/cli

# 2. Login
railway login

# 3. Inicjalizuj projekt
cd backend/TaskManager.API
railway init

# 4. Dodaj PostgreSQL (darmowa baza!)
railway add postgresql

# 5. Deploy!
railway up

# Railway automatycznie:
# - Buduje .NET aplikację
# - Tworzy bazę PostgreSQL
# - Uruchamia migrations
# - Generuje URL: https://your-app.railway.app

Frontend Deployment (Vercel przykład)

🔍 Vercel - dlaczego najlepszy dla React?
  • ✅ Zero configuration - po prostu git push!
  • ✅ Automatic HTTPS
  • ✅ Global CDN (szybkie ładowanie na całym świecie)
  • ✅ Preview deployments (każdy PR = osobny URL!)
  • ✅ Environment variables (łatwe zarządzanie)
  • ✅ Analytics
# 1. Zainstaluj Vercel CLI
npm install -g vercel

# 2. Login
vercel login

# 3. Deploy (w folderze frontend)
cd frontend
vercel

# Vercel pyta:
# - Czy to nowy projekt? → Yes
# - Framework: Vite
# - Build command: npm run build
# - Output directory: dist

# 4. Production deployment
vercel --prod

# URL: https://task-manager-xyz.vercel.app ✅

Environment Variables

🔍 Environment Variables - dlaczego ważne?

Problem: Różne URLe na dev vs production

// ❌ Hardcoded:
const API_URL = 'http://localhost:5000/api';
// Development: OK
// Production: BŁĄD! (localhost nie działa w przeglądarce użytkownika)

Rozwiązanie: Environment variables

// ✅ Environment variable:
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api';

// .env.development (local)
VITE_API_URL=http://localhost:5000/api

// .env.production (Vercel)
VITE_API_URL=https://your-api.railway.app/api

// Auto switch! ✅
# Frontend - .env.production
VITE_API_URL=https://task-manager-api.railway.app/api
// Backend - appsettings.Production.json
{
  "ConnectionStrings": {
    "DefaultConnection": "DATABASE_URL_FROM_RAILWAY"
  },
  "Jwt": {
    "Key": "PRODUCTION_SECRET_KEY_FROM_ENV",
    "Issuer": "TaskManagerAPI",
    "Audience": "TaskManagerClient"
  }
}
⚠️ NIGDY nie commituj sekretów do Git!

Dodaj do .gitignore:

.env
.env.local
.env.production
appsettings.Production.json

# ✅ Commituj TEMPLATE:
.env.example  # Bez prawdziwych wartości!

# .env.example:
VITE_API_URL=http://localhost:5000/api
# Developer wypełnia swoje wartości lokalnie

CORS na produkcji

🔍 CORS - update na produkcję
// Development (Program.cs):
builder.Services.AddCors(options => {
    options.AddPolicy("AllowFrontend", policy => {
        policy.WithOrigins("http://localhost:5173")  // ← DEV only
    });
});

// Production (trzeba dodać production URL!):
var allowedOrigins = builder.Configuration
    .GetSection("AllowedOrigins")
    .Get() ?? new[] { "http://localhost:5173" };

builder.Services.AddCors(options => {
    options.AddPolicy("AllowFrontend", policy => {
        policy.WithOrigins(allowedOrigins)  // ← DEV + PROD
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();
    });
});

// appsettings.Production.json:
{
  "AllowedOrigins": [
    "https://task-manager-xyz.vercel.app"
  ]
}
Troubleshooting - typowe błędy

⚠️ Błąd #1: CORS Error
// Console Error:
Access to XMLHttpRequest at 'http://localhost:5000/api/projects'
from origin 'http://localhost:5173' has been blocked by CORS policy

// Przyczyna:
// Backend NIE ma AddCors() lub zły origin

// Rozwiązanie:
// 1. Sprawdź Program.cs:
builder.Services.AddCors(options => {
    options.AddPolicy("AllowFrontend", policy => {
        policy.WithOrigins("http://localhost:5173")  // ← Sprawdź port!
    });
});

// 2. Sprawdź middleware:
app.UseCors("AllowFrontend");  // ← PRZED UseAuthentication!

// 3. Sprawdź czy backend działa:
curl http://localhost:5000/api/projects
// Jeśli 401 = działa! (wymaga tokenu)
⚠️ Błąd #2: 401 Unauthorized na każdym request
// Console Error:
401 Unauthorized

// Przyczyna #1: Token nie jest wysyłany
// Sprawdź Network tab → Headers:
// Authorization: Bearer   // ← Musi być!

// Rozwiązanie:
// Sprawdź Axios interceptor:
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  console.log('Token:', token);  // ← Debug!
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Przyczyna #2: Token wygasł
// Sprawdź expiration na https://jwt.io
// Wklej token → sprawdź "exp" field

// Rozwiązanie:
// Zaloguj się ponownie → nowy token
⚠️ Błąd #3: Cannot read property 'map' of undefined
// React Error:
Cannot read property 'map' of undefined

// Kod:
const { data: projects } = useProjects();
return projects.map(p => 
{p.name}
); // ← Błąd! // Przyczyna: // Podczas ładowania data = undefined! // Rozwiązanie #1: Optional chaining return projects?.map(p =>
{p.name}
); // Rozwiązanie #2: Loading state const { data: projects, isLoading } = useProjects(); if (isLoading) return
Loading...
; return projects.map(p =>
{p.name}
); // ← Bezpieczne! // Rozwiązanie #3: Default value const { data: projects = [] } = useProjects(); return projects.map(p =>
{p.name}
); // ← Zawsze array!
⚠️ Błąd #4: Entity Framework Migration błąd
// Error:
The Entity Framework tools version '8.0.0' is older than that
of the runtime '9.0.0'

// Rozwiązanie:
dotnet tool update --global dotnet-ef

// Error #2:
Build failed

// Rozwiązanie:
// Najpierw zbuduj projekt:
dotnet build
// Potem migration:
dotnet ef migrations add InitialCreate

// Error #3:
Unable to create an object of type 'AppDbContext'

// Rozwiązanie:
// Dodaj konstruktor w DbContext:
public AppDbContext(DbContextOptions options)
    : base(options)
{ }
💪 Ćwiczenie 3: Debug Flow

Przećwicz debugging Full Stack aplikacji:

  1. Otwórz DevTools → Network tab
  2. Utwórz nowe zadanie
  3. Znajdź POST /api/tasks request
  4. Sprawdź:
    • Request Headers (Authorization?)
    • Request Payload (dane poprawne?)
    • Response Status (201 Created?)
    • Response Body (TaskDto?)
  5. Sprawdź React Query DevTools:
    • ['tasks'] invalidated?
    • Refetch triggered?
    • Cache updated?
  6. W Backend:
    • Dodaj breakpoint w TasksController.Create()
    • Sprawdź dto values
    • Sprawdź userId z Claims

To nauczy Cię jak debugować problemy! 🐛

Podsumowanie - Full Stack Journey!

Gratulacje! Ukończyłeś kompletny kurs React + TypeScript + .NET - 20 obszernych wpisów! 🎉🚀

🎯 Co osiągnąłeś?

Backend Skills:

  • ✅ .NET Core Web API + Entity Framework
  • ✅ Clean Architecture (Controllers → Services → Data)
  • ✅ JWT Authentication & Authorization
  • ✅ RESTful API Design
  • ✅ Database Migrations
  • ✅ DTOs + AutoMapper

Frontend Skills:

  • ✅ React + TypeScript (Type Safety!)
  • ✅ Zustand (Global State)
  • ✅ React Query (Server State + Cache)
  • ✅ Axios + Interceptors
  • ✅ React Router + Protected Routes
  • ✅ Tailwind CSS (Rapid UI)

Full Stack Skills:

  • ✅ Frontend ↔ Backend integration
  • ✅ CORS configuration
  • ✅ Token flow (login → requests → logout)
  • ✅ Error handling (401, 404, 500)
  • ✅ Deployment (Vercel + Railway)
  • ✅ Debugging (Network, DevTools)

Co dalej? Rozwijaj Task Manager!

Dodatkowe features:

  • Real-time updates - SignalR (zadania aktualizują się live!)
  • Współdzielenie projektów - multi-user collaboration
  • Drag & Drop - react-beautiful-dnd dla Kanban board
  • Notifications - przypomnienia o deadline
  • File uploads - załączniki do zadań
  • Comments - dyskusje pod zadaniami
  • Activity log - kto co zmienił
  • Export - PDF/Excel reports
  • Analytics - wykresy produktywności
  • Dark mode - Tailwind + localStorage

Inne projekty Full Stack do zbudowania:

  • E-commerce Shop - produkty, koszyk, płatności (Stripe)
  • Blog Platform - posty, komentarze, markdown editor
  • Social Media App - posty, follow, likes, messages
  • Budget Tracker - wydatki, kategorie, wykresy
  • CRM System - klienci, deals, pipeline
  • Learning Platform - kursy, lekcje, quizy
🔍 Kluczowe takeaways z całej serii:

1. Separation of Concerns:

  • Backend = logika biznesowa, security, data
  • Frontend = UI/UX, validacja, user experience
  • API = kontrakt między nimi (RESTful)

2. Type Safety wszędzie:

  • Backend: C# (strongly typed)
  • Frontend: TypeScript (strongly typed)
  • DTOs = shared contract (te same typy!)
  • Errors caught at compile time = mniej bugów!

3. Security First:

  • JWT tokens (authentication)
  • [Authorize] attributes (authorization)
  • BCrypt dla haseł (nigdy plain text!)
  • CORS (kontrola origin)
  • HTTPS (always in production!)

4. Developer Experience:

  • React Query = zero boilerplate dla API
  • Zustand = prosty global state
  • Vite = ultra fast HMR
  • Tailwind = rapid UI development
  • TypeScript = autocomplete + IntelliSense
Doceniasz moją pracę? Wesprzyj bloga 'kupując mi kawę'.

Jeżeli seria wpisów dotycząca React była dla Ciebie pomocna, pozwoliła Ci rozwinąć obecne umiejętności lub dzięki niej nauczyłeś się czegoś nowego... będę wdzięczny za wsparcie w każdej postaci wraz z wiadomością, którą będę miał przyjemność przeczytać.

Z góry dziekuje za każde wsparcie - i pamiętajcie, wpisy były, są i będą za darmo!