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.
# 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)
);
// 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):
Pola: Id, Text, CreatedAt, TaskId, UserId
Relacja: Task ma wiele Comments (1:N)
Relacja: User ma wiele Comments (1:N)
DeleteBehavior: Cascade dla Task, Restrict dla User
Dodaj do DbContext
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
// [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ę
// 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
// 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:
Uruchom: dotnet run
Otwórz: http://localhost:5000/swagger
POST /api/auth/register - zarejestruj użytkownika
POST /api/auth/login - zaloguj się, skopiuj token
Kliknij "Authorize" → wklej token
POST /api/projects - utwórz projekt (wymaga tokenu!)
GET /api/projects - pobierz projekty
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
// 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
// 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! ✅
// 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)
{ }
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!