Table of Contents
- Hono Assignments — Kompletny przewodnik
- Wymagania wstępne
- Zadanie 01: Podstawowe API do zarządzania zadaniami (To-Do)
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 02: Blog osobisty
- Co się nauczysz
- Kluczowe pojęcia
- Instalacja
- Uruchamianie serwera
- Pliki projektu
- Testowanie krok po kroku
- Zadanie 03: Profil użytkownika i przesyłanie plików
- Zadanie 04: Menedżer zadań z procesami w tle
- Zadanie 05: Dashboard pogodowy
- Zadanie 06: Czat w czasie rzeczywistym
- Zadanie 07: Obsługa formularzy i szablony
- Zadanie 08: Odbiornik webhooków
- Zadanie 09: Zarządzanie magazynem
- Zadanie 10: Bezpieczny sejf na dokumenty
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Hono Assignments — Kompletny przewodnik
Ten przewodnik omawia wszystkie dziesięć zadań z katalogu hono-assignments/. Hono to lekki framework webowy dla języka JavaScript/TypeScript zbudowany na Web Standard APIs — tych samych obiektach Request, Response i fetch, które istnieją w przeglądarkach. Framework działa na Node.js, Cloudflare Workers, Deno i Bun. Poniższe zadania są skierowane na środowisko Node.js.
Wymagania wstępne
Wersja Node.js
Wymagany jest Node.js w wersji 18 lub nowszej. Wersja 18 wprowadziła natywne API fetch oraz tryb --watch umożliwiający automatyczne przeładowanie serwera.
node --version
npm i package.json
Każdy katalog zadania zawiera plik package.json z listą zależności projektu. Instalacja tych zależności jest zawsze pierwszym krokiem:
npm install
Polecenie tworzy katalog node_modules/ ze wszystkimi wymaganymi pakietami. Katalogu node_modules/ nie należy dodawać do systemu kontroli wersji.
Uruchamianie w trybie deweloperskim
Skrypt dev zdefiniowany w package.json każdego zadania uruchamia serwer z obsługą automatycznego przeładowania:
npm run dev
Jest to równoważne poleceniu node --watch index.js.
Narzędzia do testowania
Hono nie generuje dokumentacji Swagger. Należy korzystać z curl lub Postmana. Niektóre zadania serwują stronę HTML — w ich przypadku wystarczy przeglądarka.
Zadanie 01: Podstawowe API do zarządzania zadaniami (To-Do)
Lokalizacja: hono-assignments/01-basic-todo-api/
Co się nauczysz
- Tworzenia aplikacji Hono i definiowania tras
- Obiektu
Context(c) w Hono - Odczytu parametrów URL oraz parsowania ciał żądań JSON
- Zwracania odpowiedzi w formacie JSON
- Uruchamiania Hono na Node.js przy użyciu
@hono/node-server
Kluczowe pojęcia
Hono jest zbudowane na Web Standards. Jego API korzysta z obiektów Request i Response ze specyfikacji Fetch API — tych samych, które istnieją w przeglądarkach. Dzięki temu Hono jest przenośne między środowiskami uruchomieniowymi JavaScript (Node.js, Cloudflare Workers, Deno, Bun) przy minimalnych zmianach w kodzie.
Obiekt Context (c) jest centralnym elementem każdego handlera trasy w Hono. Udostępnia:
c.req— przychodzące żądaniec.req.param('name')— parametry ścieżki URLc.req.json()— parsowanie ciała żądania jako JSON (asynchroniczne)c.req.query('key')— parametry zapytania (query string)c.json(data, status)— zwraca odpowiedź JSONc.text(string, status)— zwraca odpowiedź w postaci czystego tekstu
@hono/node-server: Handler fetch Hono nie rozumie bezpośrednio interfejsu serwera HTTP Node.js. Adapter serve({ fetch: app.fetch }) pełni rolę pomostu: tłumaczy żądania HTTP Node.js na obiekty Request zgodne z Web Standard, wywołuje app.fetch i zapisuje zwróconą Response z powrotem do odpowiedzi Node.js.
Asynchroniczność jako wartość domyślna: W odróżnieniu od Flask (synchronicznego domyślnie), wszystkie handlery tras w Hono są funkcjami async. Parsowanie JSON, odczyt plików i wywołania HTTP wymagają await.
Instalacja
npm install
Uruchamianie serwera
npm run dev
Pliki projektu
index.js
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
let todos = []
let nextId = 1
app.post('/todos', async (c) => {
const body = await c.req.json()
const todo = { id: nextId++, title: body.title, completed: false }
todos.push(todo)
return c.json(todo, 201)
})
app.get('/todos/:id', (c) => {
const id = parseInt(c.req.param('id'))
const todo = todos.find(t => t.id === id)
if (!todo) return c.json({ error: 'Not found' }, 404)
return c.json(todo)
})
serve({ fetch: app.fetch, port: 3000 })
Parametry ścieżki używają składni :name (nie {name} jak w FastAPI ani <name> jak w Flask). Metoda c.req.param('id') zwraca ciąg znaków; konieczna jest jego konwersja za pomocą parseInt().
Testowanie krok po kroku
# Tworzenie zadania
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": "Zakupy spożywcze"}'
# Pobranie wszystkich zadań
curl http://localhost:3000/todos
# Aktualizacja zadania
curl -X PUT http://localhost:3000/todos/1 \
-H "Content-Type: application/json" \
-d '{"title": "Zakupy spożywcze i nabiał"}'
# Usunięcie zadania
curl -X DELETE http://localhost:3000/todos/1
Zadanie 02: Blog osobisty
Lokalizacja: hono-assignments/02-personal-blog/
Co się nauczysz
- Trwałego przechowywania danych przy użyciu
better-sqlite3(synchroniczny SQLite dla Node.js) - Walidacji danych wejściowych za pomocą biblioteki Zod
- Middleware
@hono/zod-validator - Obsługi naruszeń ograniczenia unikalności w SQLite
Kluczowe pojęcia
better-sqlite3 to synchroniczny sterownik SQLite dla Node.js. W odróżnieniu od większości bibliotek bazodanowych w Node.js, jego API nie jest oparte na obietnicach (Promise). Zapytania wykonują się natychmiast i bezpośrednio zwracają wyniki — bez await:
const posts = db.prepare('SELECT * FROM posts').all()
const post = db.prepare('SELECT * FROM posts WHERE slug = ?').get(slug)
Ta prostota czyni bibliotekę idealną do nauki. W zastosowaniach produkcyjnych warto jednak sięgnąć po asynchroniczny sterownik (np. @libsql/client), który nie blokuje event loop podczas operacji wejścia/wyjścia bazy danych.
Zod to biblioteka do deklarowania schematów i walidacji danych, stworzona z myślą o TypeScript. Definiujesz schemat, a Zod sprawdza, czy dane wejściowe mu odpowiadają:
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1).max(200),
slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
content: z.string().min(1)
})
Gdy walidacja się nie powiedzie, Zod generuje szczegółowe komunikaty błędów wskazujące dokładnie, które pola są nieprawidłowe i dlaczego.
@hono/zod-validator integruje Zod z Hono jako middleware. Po podłączeniu do trasy uruchamia się przed właściwym handlerem. Nieprawidłowe żądania są automatycznie odrzucane z odpowiedzią 400 Bad Request. Dla poprawnych żądań sparsowane dane są dostępne przez c.req.valid('json').
Zależności natywne: better-sqlite3 zawiera skompilowany kod C++. Jeśli instalacja się nie powiedzie, może być konieczny łańcuch narzędzi do budowania C/C++. Na macOS zainstaluj Xcode Command Line Tools: xcode-select --install.
Instalacja
npm install
Uruchamianie serwera
npm run dev
Pliki projektu
db.js
import Database from 'better-sqlite3'
const db = new Database('./blog.db')
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`)
export default db
Instrukcja CREATE TABLE IF NOT EXISTS jest idempotentna — tworzy tabelę tylko wtedy, gdy jeszcze nie istnieje, dzięki czemu można ją bezpiecznie wywołać przy każdym starcie serwera.
index.js
Handler POST używa middleware zValidator:
app.post('/posts', zValidator('json', postSchema), (c) => {
const { title, slug, content } = c.req.valid('json')
try {
const stmt = db.prepare('INSERT INTO posts (title, slug, content) VALUES (?, ?, ?)')
const result = stmt.run(title, slug, content)
return c.json({ id: result.lastInsertRowid, title, slug, content }, 201)
} catch (err) {
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
return c.json({ error: 'Slug already exists' }, 409)
}
throw err
}
})
Symbole zastępcze ? w zapytaniu SQL to zapytania parametryzowane, które chronią przed atakami SQL injection.
Testowanie krok po kroku
# Utworzenie wpisu
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title": "Pierwszy wpis", "slug": "pierwszy-wpis", "content": "Witaj, świecie!"}'
# Pobranie wpisu po slugu
curl http://localhost:3000/posts/pierwszy-wpis
# Próba wysłania nieprawidłowego slugu (zawiera spację) — Zod odrzuci go przed wywołaniem handlera
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-d '{"title": "Zły wpis", "slug": "zly wpis", "content": "..."}'
# Oczekiwany rezultat: 400 Bad Request
Zadanie 03: Profil użytkownika i przesyłanie plików
Lokalizacja: hono-assignments/03-user-profile-uploads/
Co się nauczysz
- Parsowania danych z formularzy multipart w Hono
- Pracy z obiektem
Filez Web API - Zapisywania plików na dysku przy użyciu modułu
fsNode.js - Serwowania plików statycznych przy użyciu middleware
serveStaticw Hono
Kluczowe pojęcia
c.req.parseBody() parsuje ciało żądania w formacie multipart lub URL-encoded. Zwraca zwykły obiekt, w którym każde pole jest albo ciągiem znaków (dla pól tekstowych), albo obiektem File (dla pól przesyłania pliku).
Obiekt File to Web Standard API reprezentujące plik z perspektywy przeglądarki. Aby zapisać go na dysku w Node.js, konieczna jest następująca sekwencja konwersji:
const file = body['file'] // Obiekt File z Web API
const buffer = await file.arrayBuffer() // Odczyt jako ArrayBuffer
const nodeBuffer = Buffer.from(buffer) // Konwersja na Buffer Node.js
await fs.writeFile(filePath, nodeBuffer) // Zapis na dysk
Taki łańcuch konwersji wynika z faktu, że moduł fs Node.js powstał przed standaryzacją Web Standard APIs. Obiekt ArrayBuffer ze standardu webowego musi zostać przekonwertowany na Buffer Node.js, zanim fs.writeFile będzie mógł go użyć.
serveStatic z pakietu hono/node udostępnia pliki z lokalnego katalogu przez HTTP:
app.use('/static/*', serveStatic({ root: './' }))
Pliki w katalogu ./static/ są dostępne pod adresem /static/nazwaPliku. Parametr root jest relatywny względem bieżącego katalogu roboczego (tego, z którego uruchamiasz npm run dev).
Instalacja
npm install
Testowanie krok po kroku
# Pobranie profilu
curl http://localhost:3000/profiles/jan_kowalski
# Przesłanie zdjęcia profilowego (użyj istniejącego pliku graficznego)
curl -X POST \
-F "file=@./photo.jpg" \
http://localhost:3000/profiles/jan_kowalski/upload-image
# Aktualizacja opisu (bio)
curl -X PATCH \
-F "bio=Programista JavaScript" \
http://localhost:3000/profiles/jan_kowalski
# Podgląd przesłanego zdjęcia w przeglądarce
# http://localhost:3000/static/uploads/jan_kowalski.jpg
Zadanie 04: Menedżer zadań z procesami w tle
Lokalizacja: hono-assignments/04-task-manager-bg-tasks/
Co się nauczysz
- Zasady działania event loop w JavaScript i jej konsekwencji dla pracy w tle
- Wzorca fire-and-forget dla funkcji asynchronicznych
- Dlaczego Node.js nie potrzebuje wątków do obsługi operacji wejścia/wyjścia w tle
Kluczowe pojęcia
Event loop w JavaScript to jednowątkowy model współbieżności. W każdej chwili wykonywany jest tylko jeden fragment kodu JavaScript. Gdy jednak JavaScript inicjuje operację wejścia/wyjścia (żądanie sieciowe, timer, odczyt pliku), rejestruje callback i oddaje kontrolę z powrotem do event loop. Inne operacje mogą być wykonywane w czasie oczekiwania na zakończenie I/O.
Fire-and-forget to wzorzec polegający na uruchomieniu funkcji asynchronicznej bez oczekiwania na jej wynik. W JavaScript wygląda to następująco:
// Z await — handler trasy czeka na zakończenie sendEmail
await sendTaskNotification(email, title)
return c.json(result)
// Bez await — sendEmail startuje, a handler trasy kontynuuje natychmiast
sendTaskNotification(email, title) // brak await
return c.json(result)
Gdy sendTaskNotification napotka swoje pierwsze await (w tym zadaniu: setTimeout), zawiesza się i oddaje sterowanie do event loop. Handler trasy w tym momencie już zwrócił swoją odpowiedź, a więc klient HTTP otrzymał ją bez czekania. Funkcja powiadamiająca wznawia działanie po upływie limitu czasu timera.
W odróżnieniu od wątków Pythona, nie ma tu prawdziwej współbieżności — callback setTimeout i kolejne żądanie HTTP na zmianę korzystają z tego samego wątku. Do zadań wymagających dużej mocy obliczeniowej w tle konieczne byłoby użycie Worker Threads.
emailUtils.js
export async function sendTaskNotification(email, taskTitle) {
console.log('Starting background task...')
await new Promise(resolve => setTimeout(resolve, 5000)) // symuluje 5-sekundowe wysyłanie e-maila
console.log(`Notification SENT to ${email}: task '${taskTitle}' completed.`)
}
Wyrażenie new Promise(resolve => setTimeout(resolve, 5000)) tworzy obietnicę, która zostaje spełniona po 5 sekundach. Jest to standardowy wzorzec asynchronicznych opóźnień w JavaScript (nie istnieje odpowiednik time.sleep).
Instalacja
npm install
Testowanie krok po kroku
- Uruchom serwer.
- Utwórz zadanie:
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title": "Wdrożenie", "user_email": "dev@example.com"}' - Zanotuj ID zadania.
- Oznacz je jako ukończone:
curl -X POST http://localhost:3000/tasks/1/complete - Obserwuj terminal — odpowiedź nadchodzi natychmiast, a pięć sekund później w logach pojawia się komunikat o wysłaniu powiadomienia.
Zadanie 05: Dashboard pogodowy
Lokalizacja: hono-assignments/05-weather-dashboard/
Co się nauczysz
- Korzystania z natywnego API
fetchdo wywoływania zewnętrznych serwisów - Walidacji i wymuszania typów parametrów zapytania przy użyciu Zod
- Budowania pamięci podręcznej (cache) w pamięci operacyjnej z obsługą TTL przy użyciu JavaScript
Map
Kluczowe pojęcia
fetch jest wbudowane w Node.js 18+. W przeciwieństwie do Pythona, do wykonywania żądań HTTP nie jest potrzebna żadna dodatkowa biblioteka. Wzorzec użycia:
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
const data = await response.json()
Właściwość response.ok ma wartość true dla kodów statusu 200–299. Metoda response.json() sama w sobie jest asynchroniczna — odczytuje ciało odpowiedzi i je parsuje.
z.coerce.number() w Zod wymusza konwersję danych wejściowych na liczbę przed walidacją. Parametry zapytania w HTTP są zawsze ciągami znaków (?lat=51.5 dociera jako napis "51.5"). Bez wymuszenia konwersji z.number() odrzuciłoby ten ciąg. Przy użyciu z.coerce.number() Zod najpierw wywołuje Number("51.5"), a następnie waliduje wynik.
JavaScript Map to uporządkowana kolekcja par klucz-wartość, która akceptuje dowolny typ jako klucz (w odróżnieniu od zwykłych obiektów, które wymuszają konwersję kluczy na ciągi znaków). Cache używa klucza w postaci ciągu "round(lat)_round(lon)".
Instalacja
npm install
Pliki projektu
weatherService.js
export class WeatherService {
async getWeather(lat, lon) {
const url = new URL('https://api.open-meteo.com/v1/forecast')
url.searchParams.set('latitude', lat)
url.searchParams.set('longitude', lon)
url.searchParams.set('current_weather', 'true')
const response = await fetch(url.toString())
if (!response.ok) throw new Error(`Weather API error: ${response.status}`)
return response.json()
}
}
index.js (logika cache)
const weatherCache = new Map()
// Sprawdzenie cache
const cacheKey = `${Math.round(lat * 10) / 10}_${Math.round(lon * 10) / 10}`
const cached = weatherCache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < 15 * 60 * 1000) {
return c.json({ ...cached.data, cached: true })
}
// Pobranie danych i zapisanie w cache
const data = await weatherService.getWeather(lat, lon)
weatherCache.set(cacheKey, { data, timestamp: Date.now() })
return c.json({ ...data, cached: false })
Funkcja Date.now() zwraca liczbę milisekund od początku epoki Unix. Porównanie TTL używa wartości 15 * 60 * 1000 (15 minut wyrażonych w milisekundach).
Testowanie krok po kroku
# Pierwsze wywołanie — dane z zewnętrznego API
curl "http://localhost:3000/weather?city_name=Warsaw&lat=52.2&lon=21.0"
# Drugie wywołanie — dane z cache
curl "http://localhost:3000/weather?city_name=Warsaw&lat=52.2&lon=21.0"
# Sprawdź w odpowiedzi: "cached": true
Zadanie 06: Czat w czasie rzeczywistym
Lokalizacja: hono-assignments/06-real-time-chat/
Co się nauczysz
- Obsługi WebSocket w Hono przy użyciu
@hono/node-ws - Procedury upgrade'u połączenia HTTP do WebSocket
- Zarządzania kolekcją aktywnych połączeń
- Rozgłaszania (broadcast) wiadomości do wszystkich podłączonych klientów
Kluczowe pojęcia
@hono/node-ws zapewnia obsługę WebSocket dla Hono na Node.js. W odróżnieniu od Flask-SocketIO (który korzysta z protokołu Socket.IO), ten adapter używa czystych WebSocketów. Klient JavaScript na stronie używa bezpośrednio new WebSocket(url) — bez żadnych dodatkowych bibliotek.
upgradeWebSocket to helper Hono obsługujący procedurę upgrade'u połączenia HTTP do WebSocket. Zwraca obiekt z funkcjami hook'ów cyklu życia:
import { createNodeWebSocket } from '@hono/node-ws'
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
app.get('/ws', upgradeWebSocket((c) => ({
onOpen(event, ws) { /* klient połączony */ },
onMessage(event, ws) { /* odebrano wiadomość; event.data zawiera jej treść */ },
onClose(event, ws) { /* klient rozłączony */ }
})))
injectWebSocket(server) musi zostać wywołane z instancją serwera HTTP Node.js, aby umożliwić upgrade połączeń WebSocket na tym serwerze.
Broadcast: Nie istnieje wbudowana funkcja broadcast. Należy utrzymywać Map aktywnych połączeń i iterować po niej:
const activeConnections = new Map()
function broadcast(message, senderId) {
for (const [clientId, ws] of activeConnections) {
if (clientId !== senderId) {
ws.send(message)
}
}
}
Instalacja
npm install
Testowanie krok po kroku
- Uruchom serwer.
- Otwórz
http://localhost:3000/w dwóch zakładkach przeglądarki. - Zwróć uwagę na różne ID klientów.
- Wpisz wiadomość w jednej zakładce i wyślij. Wiadomość pojawi się w drugiej zakładce.
- Zamknij jedną zakładkę. W drugiej pojawi się komunikat o rozłączeniu.
Zadanie 07: Obsługa formularzy i szablony
Lokalizacja: hono-assignments/07-forms-templates/
Co się nauczysz
- Renderowania HTML po stronie serwera w Hono bez silnika szablonów
- Obsługi przesyłania formularzy przy użyciu
c.req.parseBody() - Walidacji danych po stronie serwera w JavaScript
- Kompromisów między silnikami szablonów a inline'owymi ciągami HTML
Kluczowe pojęcia
Hono nie posiada wbudowanego silnika szablonów. HTML jest generowany jako JavaScript template literals — wieloliniowe ciągi znaków ograniczone odwrotnymi apostrofami, obsługujące interpolację ${wyrażenie}. Podejście to nie wymaga konfiguracji, ale przy rozbudowanych szablonach może stać się trudne w utrzymaniu.
const page = `
<!DOCTYPE html>
<html>
<body>
<h1>Witaj, ${name}!</h1>
${errors.name ? `<p class="error">${errors.name}</p>` : ''}
</body>
</html>
`
return c.html(page)
Metoda c.html(string) zwraca odpowiedź z nagłówkiem Content-Type: text/html.
Uwaga bezpieczeństwa — wstrzykiwanie HTML: Przy wstawianiu danych wprowadzonych przez użytkownika do ciągów HTML konieczne jest escapowanie znaków <, >, & i ". W tym zadaniu dane wejściowe są krótkotrwałe (nie są trwale przechowywane), więc ryzyko jest ograniczone. W aplikacji produkcyjnej należy korzystać z odpowiedniego silnika szablonów, który domyślnie wykonuje escapowanie, lub ze stosownej funkcji pomocniczej:
function escapeHtml(str) {
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
}
c.req.parseBody() obsługuje zarówno formularze multipart/form-data, jak i application/x-www-form-urlencoded. Formularze HTML domyślnie używają formatu application/x-www-form-urlencoded. Oba formaty są obsługiwane przez to samo wywołanie.
Instalacja
npm install
Testowanie krok po kroku
- Otwórz
http://localhost:3000/. - Prześlij formularz z prawidłowym imieniem i adresem e-mail. Zwróć uwagę na stronę sukcesu.
- Prześlij formularz z imieniem składającym się z jednego znaku. Sprawdź komunikat błędu wyświetlany inline oraz zachowaną wartość adresu e-mail.
- Prześlij formularz z wartością
nieprawidlowy-email. Sprawdź komunikat błędu e-mail oraz zachowaną wartość imienia.
Zadanie 08: Odbiornik webhooków
Lokalizacja: hono-assignments/08-webhook-receiver/
Co się nauczysz
- Odczytu surowego ciała żądania jako ciągu znaków w Hono
- Weryfikacji podpisu HMAC przy użyciu modułu
cryptoNode.js - Porównania stałoczasowego (constant-time) za pomocą
crypto.timingSafeEqual - Dlaczego parsowanie JSON musi następować po weryfikacji podpisu
Kluczowe pojęcia
Koncepcje webhooków, HMAC i ataków czasowych (timing attacks) opisano w przewodniku FastAPI, w zadaniu 08.
Moduł crypto Node.js udostępnia prymitywy kryptograficzne. Obliczanie HMAC:
import crypto from 'node:crypto'
function verifySignature(payload, secret, signature) {
const expected = crypto.createHmac('sha256', secret)
.update(payload)
.digest('hex')
// timingSafeEqual wymaga buforów o jednakowej długości
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(signature, 'hex')
if (a.length !== b.length) return false
return crypto.timingSafeEqual(a, b)
}
c.req.text() odczytuje ciało żądania jako ciąg znaków UTF-8 — surowe bajty bez żadnego parsowania. Jest używane do weryfikacji HMAC. Po pomyślnym sprawdzeniu payload jest parsowany ręcznie: JSON.parse(rawBody).
crypto.timingSafeEqual wymaga dwóch obiektów Buffer o jednakowej długości. Zawsze należy sprawdzić długość przed jego wywołaniem, ponieważ jeśli dostarczony podpis różni się długością od oczekiwanego, timingSafeEqual zgłosi błąd.
Instalacja
npm install
Testowanie krok po kroku
Utwórz plik test_webhook.js:
import crypto from 'node:crypto'
const secret = "super-secret-webhook-key-123"
const payload = JSON.stringify({ event: "payment.success", data: { id: 123 } })
const signature = crypto.createHmac('sha256', secret).update(payload).digest('hex')
const response = await fetch("http://localhost:3000/webhook", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-webhook-signature": signature
},
body: payload
})
console.log(await response.json())
Uruchom poleceniem: node --input-type=module < test_webhook.js lub zapisz jako test_webhook.mjs i uruchom node test_webhook.mjs.
Zadanie 09: Zarządzanie magazynem
Lokalizacja: hono-assignments/09-inventory-management/
Co się nauczysz
- Wzorca pod-aplikacji (sub-application) w Hono do organizacji kodu
- Walidacji i wymuszania typów parametrów zapytania przy użyciu Zod
- Wersjonowania API za pomocą montowanych pod-aplikacji
- Paginacji z metadanymi
Kluczowe pojęcia
Pod-aplikacje Hono: Nowa instancja Hono jest pod-aplikacją. Można ją zamontować w aplikacji nadrzędnej przy użyciu app.route('/prefix', subApp). Trasy zdefiniowane w subApp są dostępne pod adresem /prefix/trasa.
// src/v1/router.js
const v1Router = new Hono()
v1Router.get('/items', handler)
export { v1Router }
// index.js
import { v1Router } from './src/v1/router.js'
app.route('/v1', v1Router)
Jest to odpowiednik Flask Blueprints oraz APIRouter z FastAPI.
z.coerce.number().int().min(0) w schemacie zapytania wymusza konwersję parametrów query string na liczby całkowite i sprawdza, czy są nieujemne. Eliminuje to całą klasę błędów, w których ujemne wartości przesunięcia (offset) lub nieliczbowe wartości limitu powodowałyby nieprzewidywalne zachowanie podczas wycinania tablic.
Instalacja
npm install
Testowanie krok po kroku
# V1: prosta tablica
curl "http://localhost:3000/v1/items?offset=0&limit=5"
# V2: odpowiedź ze strukturą
curl "http://localhost:3000/v2/items?offset=0&limit=5"
# V2: filtrowanie po kategorii
curl "http://localhost:3000/v2/items?category=Electronics&offset=0&limit=10"
Zwróć uwagę na różnicę strukturalną: V1 zwraca [{...}, {...}], V2 zwraca {"items": [...], "total": N, "has_more": true/false}.
Zadanie 10: Bezpieczny sejf na dokumenty
Lokalizacja: hono-assignments/10-secure-document-vault/
Co się nauczysz
- Uwierzytelniania JWT przy użyciu wbudowanego middleware
hono/jwt - Haszowania haseł za pomocą
bcryptw Node.js - Tworzenia fabryki middleware do wymuszania zakresów (scopes)
- Przekazywania danych użytkownika przez system kontekstu Hono
Kluczowe pojęcia
Koncepcje JWT, haszowania haseł, zakresów OAuth2 i RBAC opisano w przewodniku FastAPI, w zadaniu 10. Poniżej omówiono wzorce implementacyjne specyficzne dla Hono.
hono/jwt udostępnia dwie funkcje:
sign(payload, secret)— tworzy token JWTjwt({ secret })— middleware weryfikujące token Bearer i przechowujące zdekodowany payload w kontekście
import { sign, jwt } from 'hono/jwt'
// Middleware: weryfikuje token i zapisuje payload w kontekście
const requireAuth = jwt({ secret: SECRET })
// Handler trasy: odczytuje zdekodowany payload
app.get('/protected', requireAuth, (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload.sub })
})
c.set i c.get: Kontekst Hono posiada typowany magazyn klucz-wartość służący do przekazywania danych między funkcjami middleware a handlerami. c.set('user', userObject) zapisuje użytkownika w kontekście; c.get('user') pobiera go dalej w łańcuchu przetwarzania.
Fabryka middleware dla zakresów:
function requireScopes(requiredScopes) {
return async (c, next) => {
const payload = c.get('jwtPayload')
const tokenScopes = payload.scopes || []
const hasAll = requiredScopes.every(s => tokenScopes.includes(s))
if (!hasAll) return c.json({ error: 'Insufficient permissions' }, 403)
// Pobranie użytkownika i przypisanie go do kontekstu
const user = users.find(u => u.username === payload.sub)
c.set('user', user)
await next()
}
}
Łączenie middleware w łańcuch na trasie:
app.get('/documents', requireAuth, requireScopes(['vault:read']), (c) => {
const user = c.get('user')
// filtrowanie dokumentów według poziomu dostępu...
})
bcrypt: Pakiet bcrypt zapewnia haszowanie haseł. Podobnie jak better-sqlite3, jest to natywny moduł Node.js wymagający łańcucha narzędzi do budowania C++.
import bcrypt from 'bcrypt'
const hash = await bcrypt.hash('password123', 10) // 10 = współczynnik kosztu
const valid = await bcrypt.compare('password123', hash) // true
Współczynnik kosztu (10) kontroluje liczbę wykonywanych rund haszowania. Wyższe wartości spowalniają operację haszowania, zwiększając odporność na ataki siłowe (brute-force).
Instalacja
npm install
Testowanie krok po kroku
# Uwierzytelnienie jako alice
curl -X POST http://localhost:3000/token \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "alice123"}'
# Zapisz token, a następnie:
TOKEN="<wklej_token_tutaj>"
# Odczyt dokumentów (alice widzi dokumenty o poziomie dostępu ≤ 3)
curl http://localhost:3000/documents \
-H "Authorization: Bearer $TOKEN"
# Próba utworzenia dokumentu powyżej poziomu dostępu alice
curl -X POST http://localhost:3000/documents \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Ściśle tajne", "content": "...", "secret_level": 4}'
# Oczekiwany rezultat: 403
# Próba dostępu do endpointu admina jako alice
curl http://localhost:3000/admin/users \
-H "Authorization: Bearer $TOKEN"
# Oczekiwany rezultat: 403 (alice nie posiada zakresu 'admin')
Powtórz testy z danymi admin / admin123, aby zweryfikować pełny dostęp administratora.