Cloudflare - Kompletny Przewodnik po Edge Computing
Czym jest Cloudflare?
Cloudflare to globalna platforma edge computing, która zrewolucjonizowała sposób, w jaki budujemy i dostarczamy aplikacje internetowe. Rozpoczynając jako usługa CDN i ochrony DDoS, Cloudflare przekształciło się w pełnoprawną platformę developerską z Workers (serverless na edge), Pages (hosting statyczny i SSR), D1 (baza danych SQLite), R2 (object storage) i wieloma innymi usługami.
Kluczowa różnica Cloudflare to edge computing - Twój kod działa w ponad 300 lokalizacjach na całym świecie, blisko użytkowników, zapewniając ultra-niskie latency (często poniżej 50ms).
Dlaczego Cloudflare?
Kluczowe zalety
- Globalny edge - 300+ lokalizacji, < 50ms latency globalnie
- Zero cold starts - Workers uruchamiają się natychmiast
- Hojny free tier - 100k req/day na Workers, 10GB R2
- Zintegrowany ekosystem - Workers + D1 + R2 + KV współpracują seamlessly
- Bezpieczeństwo - WAF, DDoS protection, Bot Management wbudowane
- Developer Experience - Wrangler CLI, lokalne środowisko dev
Cloudflare vs AWS/Vercel/Netlify
| Cecha | Cloudflare | AWS Lambda@Edge | Vercel | Netlify |
|---|---|---|---|---|
| Edge locations | 300+ | 13 (Lambda@Edge) | ~20 | ~20 |
| Cold start | 0ms | 100-500ms | ~50ms | ~100ms |
| Free tier | 100k req/day | Płatne | 100k req/mo | 125k req/mo |
| Database | D1 (SQLite) | DynamoDB | Postgres (Neon) | Brak natywnej |
| Storage | R2 (no egress) | S3 (egress fee) | Blob | Brak |
| Pricing model | Pay-per-request | Pay-per-duration | Bandwidth | Bandwidth |
Kiedy wybrać Cloudflare?
- API na edge - Ultra-niskie latency dla globalnych użytkowników
- Statyczne strony - Pages z CDN i cache
- Full-stack apps - Workers + D1 + R2
- Bezpieczeństwo - WAF, rate limiting, bot protection
- Cost optimization - R2 bez opłat za egress
Cloudflare Workers
Czym są Workers?
Workers to serverless functions działające na edge Cloudflare - w ponad 300 lokalizacjach na świecie. W przeciwieństwie do tradycyjnych Lambda, Workers używają V8 Isolates zamiast kontenerów, co oznacza zero cold starts.
Tworzenie projektu Workers
# Instalacja Wrangler CLI
npm install -g wrangler
# Logowanie
wrangler login
# Tworzenie projektu
npm create cloudflare@latest my-worker
# Lub z template
npm create cloudflare@latest my-api -- --template honoPodstawowy Worker
// src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// Routing
if (url.pathname === '/') {
return new Response('Hello from Cloudflare Workers!', {
headers: { 'Content-Type': 'text/plain' },
})
}
if (url.pathname === '/api/data') {
return Response.json({
message: 'Hello from the edge!',
location: request.cf?.city || 'Unknown',
timestamp: new Date().toISOString(),
})
}
if (url.pathname.startsWith('/api/users')) {
return handleUsers(request, env)
}
return new Response('Not Found', { status: 404 })
},
} satisfies ExportedHandler<Env>
async function handleUsers(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const userId = url.pathname.split('/')[3]
switch (request.method) {
case 'GET':
if (userId) {
// Get single user
const user = await env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(userId).first()
if (!user) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
return Response.json(user)
}
// Get all users
const { results } = await env.DB.prepare('SELECT * FROM users').all()
return Response.json(results)
case 'POST':
const body = await request.json<{ name: string; email: string }>()
const result = await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *'
).bind(body.name, body.email).first()
return Response.json(result, { status: 201 })
default:
return new Response('Method not allowed', { status: 405 })
}
}Konfiguracja wrangler.toml
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# Node.js compatibility
compatibility_flags = ["nodejs_compat"]
# Environment variables
[vars]
API_VERSION = "1.0.0"
# Secrets (dodaj przez CLI: wrangler secret put API_KEY)
# API_KEY = "..."
# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxx-xxxx-xxxx-xxxx"
# KV Namespace
[[kv_namespaces]]
binding = "KV"
id = "xxxxx-xxxx-xxxx-xxxx"
# R2 Bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
# Durable Objects
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
# Cron Triggers
[triggers]
crons = ["0 * * * *"] # Every hour
# Routes
routes = [
{ pattern = "api.example.com/*", zone_name = "example.com" }
]
# Preview environment
[env.preview]
name = "my-worker-preview"
vars = { ENVIRONMENT = "preview" }
# Production environment
[env.production]
name = "my-worker-production"
vars = { ENVIRONMENT = "production" }
routes = [
{ pattern = "api.example.com/*", zone_name = "example.com" }
]TypeScript types
// src/types.ts
export interface Env {
// D1 Database
DB: D1Database
// KV Namespace
KV: KVNamespace
// R2 Bucket
BUCKET: R2Bucket
// Durable Object
COUNTER: DurableObjectNamespace
// Environment variables
API_VERSION: string
API_KEY: string
}
// Request with Cloudflare properties
interface CfProperties {
city?: string
country?: string
continent?: string
latitude?: string
longitude?: string
timezone?: string
region?: string
asn?: number
colo?: string
}
declare global {
interface Request {
cf?: CfProperties
}
}Hono Framework (Recommended)
npm create cloudflare@latest my-api -- --template hono// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'
type Bindings = {
DB: D1Database
KV: KVNamespace
JWT_SECRET: string
}
const app = new Hono<{ Bindings: Bindings }>()
// Middleware
app.use('*', logger())
app.use('/api/*', cors())
// Public routes
app.get('/', (c) => c.text('Hello Hono on Workers!'))
app.get('/api/health', (c) => {
return c.json({
status: 'ok',
timestamp: new Date().toISOString(),
location: c.req.raw.cf?.city || 'Unknown',
})
})
// Protected routes
app.use('/api/protected/*', jwt({ secret: 'your-secret' }))
app.get('/api/protected/profile', async (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})
// CRUD for users
app.get('/api/users', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT * FROM users ORDER BY created_at DESC'
).all()
return c.json(results)
})
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id')
const user = await c.env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(id).first()
if (!user) {
return c.json({ error: 'User not found' }, 404)
}
return c.json(user)
})
app.post('/api/users', async (c) => {
const body = await c.req.json<{ name: string; email: string }>()
const result = await c.env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *'
).bind(body.name, body.email).first()
return c.json(result, 201)
})
app.put('/api/users/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json<{ name?: string; email?: string }>()
const result = await c.env.DB.prepare(
'UPDATE users SET name = COALESCE(?, name), email = COALESCE(?, email) WHERE id = ? RETURNING *'
).bind(body.name, body.email, id).first()
return c.json(result)
})
app.delete('/api/users/:id', async (c) => {
const id = c.req.param('id')
await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run()
return c.body(null, 204)
})
// Error handling
app.onError((err, c) => {
console.error(`Error: ${err.message}`)
return c.json({ error: err.message }, 500)
})
app.notFound((c) => {
return c.json({ error: 'Not found' }, 404)
})
export default appLokalny development
# Uruchom lokalnie z wrangler
wrangler dev
# Z hot reload
wrangler dev --live-reload
# Z lokalnymi bindings (D1, KV, R2)
wrangler dev --local --persist
# Na konkretnym porcie
wrangler dev --port 8787Deploy
# Deploy do Cloudflare
wrangler deploy
# Deploy do konkretnego środowiska
wrangler deploy --env production
# Preview deploy
wrangler deploy --env previewD1 - SQLite na Edge
Tworzenie bazy D1
# Utwórz bazę
wrangler d1 create my-database
# Skopiuj ID do wrangler.toml
# database_id = "xxxxx-xxxx-xxxx-xxxx"Schema i migracje
-- migrations/0001_create_users.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
-- migrations/0002_create_posts.sql
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
author_id INTEGER NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users(id)
);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_published ON posts(published);# Uruchom migracje
wrangler d1 migrations apply my-database
# Lokalnie
wrangler d1 migrations apply my-database --localQueries w D1
// Podstawowe operacje
async function d1Examples(db: D1Database) {
// SELECT wszystko
const { results: allUsers } = await db
.prepare('SELECT * FROM users')
.all()
// SELECT z parametrami (prepared statement)
const user = await db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(1)
.first()
// SELECT z wieloma parametrami
const filteredUsers = await db
.prepare('SELECT * FROM users WHERE name LIKE ? AND created_at > ?')
.bind('%John%', '2024-01-01')
.all()
// INSERT
const insertResult = await db
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind('John Doe', 'john@example.com')
.run()
console.log('Inserted ID:', insertResult.meta.last_row_id)
// INSERT RETURNING
const newUser = await db
.prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')
.bind('Jane Doe', 'jane@example.com')
.first()
// UPDATE
const updateResult = await db
.prepare('UPDATE users SET name = ? WHERE id = ?')
.bind('John Smith', 1)
.run()
console.log('Rows changed:', updateResult.meta.changes)
// DELETE
await db.prepare('DELETE FROM users WHERE id = ?').bind(1).run()
// Batch operations (transaction)
const batchResults = await db.batch([
db.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('User 1', 'user1@example.com'),
db.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('User 2', 'user2@example.com'),
db.prepare('UPDATE posts SET published = TRUE WHERE author_id = ?').bind(1),
])
// Raw SQL (for complex queries)
const { results } = await db.prepare(`
SELECT
u.id,
u.name,
COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10
`).all()
return results
}D1 z Drizzle ORM
npm install drizzle-orm
npm install -D drizzle-kit// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: text('created_at').default('CURRENT_TIMESTAMP'),
})
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
published: integer('published', { mode: 'boolean' }).default(false),
})// db/index.ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'
export function createDb(d1: D1Database) {
return drizzle(d1, { schema })
}
// Użycie w Worker
import { Hono } from 'hono'
import { createDb } from './db'
import { users } from './db/schema'
import { eq } from 'drizzle-orm'
const app = new Hono<{ Bindings: { DB: D1Database } }>()
app.get('/users', async (c) => {
const db = createDb(c.env.DB)
const allUsers = await db.select().from(users)
return c.json(allUsers)
})
app.post('/users', async (c) => {
const db = createDb(c.env.DB)
const body = await c.req.json<{ name: string; email: string }>()
const [newUser] = await db.insert(users).values(body).returning()
return c.json(newUser, 201)
})Workers KV
Tworzenie namespace
# Utwórz KV namespace
wrangler kv:namespace create "MY_KV"
# Preview namespace
wrangler kv:namespace create "MY_KV" --previewOperacje KV
interface Env {
KV: KVNamespace
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// GET - odczyt
const value = await env.KV.get('key')
const jsonValue = await env.KV.get('user:123', { type: 'json' })
const binaryValue = await env.KV.get('image', { type: 'arrayBuffer' })
// PUT - zapis
await env.KV.put('key', 'value')
// PUT z opcjami
await env.KV.put('session:abc', JSON.stringify({ userId: 1 }), {
expirationTtl: 3600, // 1 godzina
metadata: { createdAt: Date.now() },
})
// PUT z expiration timestamp
await env.KV.put('temp-key', 'value', {
expiration: Math.floor(Date.now() / 1000) + 86400, // Unix timestamp
})
// DELETE
await env.KV.delete('key')
// LIST - listowanie kluczy
const { keys, list_complete, cursor } = await env.KV.list({
prefix: 'user:',
limit: 100,
})
// GET with metadata
const { value: val, metadata } = await env.KV.getWithMetadata('key')
return Response.json({ value, metadata })
},
}Cache Pattern z KV
async function getCached<T>(
kv: KVNamespace,
key: string,
fetcher: () => Promise<T>,
ttl = 3600
): Promise<T> {
// Sprawdź cache
const cached = await kv.get(key, { type: 'json' })
if (cached) {
return cached as T
}
// Pobierz dane
const data = await fetcher()
// Zapisz do cache
await kv.put(key, JSON.stringify(data), {
expirationTtl: ttl,
})
return data
}
// Użycie
const user = await getCached(
env.KV,
`user:${userId}`,
() => fetchUserFromDB(userId),
3600
)R2 - Object Storage
Tworzenie bucket
# Utwórz bucket
wrangler r2 bucket create my-bucketOperacje R2
interface Env {
BUCKET: R2Bucket
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const key = url.pathname.slice(1)
switch (request.method) {
case 'GET': {
// Pobierz obiekt
const object = await env.BUCKET.get(key)
if (!object) {
return new Response('Object not found', { status: 404 })
}
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag)
return new Response(object.body, { headers })
}
case 'PUT': {
// Upload obiektu
const contentType = request.headers.get('content-type') || 'application/octet-stream'
await env.BUCKET.put(key, request.body, {
httpMetadata: {
contentType,
},
customMetadata: {
uploadedAt: new Date().toISOString(),
},
})
return new Response(`Uploaded ${key}`, { status: 201 })
}
case 'DELETE': {
await env.BUCKET.delete(key)
return new Response(`Deleted ${key}`, { status: 200 })
}
default:
return new Response('Method not allowed', { status: 405 })
}
},
}Presigned URLs
import { AwsClient } from 'aws4fetch'
interface Env {
R2_ACCESS_KEY_ID: string
R2_SECRET_ACCESS_KEY: string
R2_BUCKET_NAME: string
R2_ACCOUNT_ID: string
}
async function generatePresignedUrl(
env: Env,
key: string,
expiresIn = 3600
): Promise<string> {
const client = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
})
const endpoint = `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
const url = new URL(`${endpoint}/${env.R2_BUCKET_NAME}/${key}`)
const signed = await client.sign(new Request(url, { method: 'GET' }), {
aws: { signQuery: true },
})
return signed.url
}Image Upload Endpoint
import { Hono } from 'hono'
import { v4 as uuid } from 'uuid'
const app = new Hono<{ Bindings: { BUCKET: R2Bucket } }>()
app.post('/upload', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file') as File | null
if (!file) {
return c.json({ error: 'No file provided' }, 400)
}
// Walidacja typu pliku
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
if (!allowedTypes.includes(file.type)) {
return c.json({ error: 'Invalid file type' }, 400)
}
// Walidacja rozmiaru (5MB max)
if (file.size > 5 * 1024 * 1024) {
return c.json({ error: 'File too large' }, 400)
}
// Generuj unikalną nazwę pliku
const ext = file.name.split('.').pop()
const key = `uploads/${uuid()}.${ext}`
// Upload do R2
await c.env.BUCKET.put(key, file.stream(), {
httpMetadata: {
contentType: file.type,
},
})
return c.json({
key,
url: `https://cdn.example.com/${key}`,
})
})
export default appCloudflare Pages
Deploy statycznej strony
# Z folderu dist
wrangler pages deploy ./dist
# Z GitHub (automatyczne deploye)
# Połącz repo w Cloudflare DashboardPages z Next.js
# Dodaj adapter
npm install @cloudflare/next-on-pages
# Build
npx @cloudflare/next-on-pages
# Deploy
wrangler pages deploy .vercel/output/static// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
runtime: 'edge',
},
}
module.exports = nextConfigPages Functions (API routes)
project/
├── functions/
│ ├── api/
│ │ ├── users.ts # /api/users
│ │ └── users/
│ │ └── [id].ts # /api/users/:id
│ └── _middleware.ts # Global middleware
├── public/
└── dist/// functions/api/users.ts
interface Env {
DB: D1Database
}
export const onRequestGet: PagesFunction<Env> = async (context) => {
const { results } = await context.env.DB
.prepare('SELECT * FROM users')
.all()
return Response.json(results)
}
export const onRequestPost: PagesFunction<Env> = async (context) => {
const body = await context.request.json<{ name: string; email: string }>()
const result = await context.env.DB
.prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')
.bind(body.name, body.email)
.first()
return Response.json(result, { status: 201 })
}// functions/_middleware.ts
export const onRequest: PagesFunction = async (context) => {
// CORS headers
const response = await context.next()
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return response
}Durable Objects
Czym są Durable Objects?
Durable Objects to unikalna funkcja Cloudflare pozwalająca na stateful computing na edge. Każdy Durable Object ma:
- Unikalny ID
- Trwały storage (transactional)
- Single-threaded execution
- WebSocket support
Counter przykład
// src/counter.ts
export class Counter implements DurableObject {
private value: number = 0
private state: DurableObjectState
constructor(state: DurableObjectState, env: Env) {
this.state = state
// Odczytaj wartość z storage przy starcie
this.state.blockConcurrencyWhile(async () => {
const stored = await this.state.storage.get<number>('value')
this.value = stored || 0
})
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url)
switch (url.pathname) {
case '/increment':
this.value++
await this.state.storage.put('value', this.value)
return Response.json({ value: this.value })
case '/decrement':
this.value--
await this.state.storage.put('value', this.value)
return Response.json({ value: this.value })
case '/':
return Response.json({ value: this.value })
default:
return new Response('Not found', { status: 404 })
}
}
}
// src/index.ts
export { Counter } from './counter'
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const counterId = url.searchParams.get('id') || 'default'
// Pobierz lub utwórz Durable Object
const id = env.COUNTER.idFromName(counterId)
const counter = env.COUNTER.get(id)
// Przekaż request do Durable Object
return counter.fetch(request)
},
}WebSocket z Durable Objects
export class ChatRoom implements DurableObject {
private sessions: Map<WebSocket, { username: string }> = new Map()
private state: DurableObjectState
constructor(state: DurableObjectState, env: Env) {
this.state = state
}
async fetch(request: Request): Promise<Response> {
if (request.headers.get('Upgrade') !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 })
}
const { 0: client, 1: server } = new WebSocketPair()
const username = new URL(request.url).searchParams.get('username') || 'Anonymous'
this.sessions.set(server, { username })
server.accept()
server.addEventListener('message', (event) => {
const message = {
username,
text: event.data,
timestamp: new Date().toISOString(),
}
// Broadcast do wszystkich
this.broadcast(JSON.stringify(message))
})
server.addEventListener('close', () => {
this.sessions.delete(server)
this.broadcast(JSON.stringify({
type: 'leave',
username,
}))
})
// Powiadom o dołączeniu
this.broadcast(JSON.stringify({
type: 'join',
username,
}))
return new Response(null, {
status: 101,
webSocket: client,
})
}
private broadcast(message: string) {
for (const [ws] of this.sessions) {
try {
ws.send(message)
} catch (e) {
this.sessions.delete(ws)
}
}
}
}Queues
Tworzenie kolejki
wrangler queues create my-queueProducer (wysyłanie)
interface Env {
MY_QUEUE: Queue
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Wyślij pojedynczą wiadomość
await env.MY_QUEUE.send({
type: 'email',
to: 'user@example.com',
subject: 'Hello',
body: 'World',
})
// Wyślij batch
await env.MY_QUEUE.sendBatch([
{ body: { task: 1 } },
{ body: { task: 2 } },
{ body: { task: 3 } },
])
return new Response('Queued!')
},
}Consumer (odbieranie)
interface Env {
MY_QUEUE: Queue
}
export default {
async queue(batch: MessageBatch, env: Env): Promise<void> {
for (const message of batch.messages) {
try {
const data = message.body as { type: string; to: string }
if (data.type === 'email') {
await sendEmail(data)
}
// Potwierdź przetworzenie
message.ack()
} catch (error) {
// Retry później
message.retry()
}
}
},
}Scheduled Workers (Cron)
# wrangler.toml
[triggers]
crons = [
"0 * * * *", # Co godzinę
"0 0 * * *", # Codziennie o północy
"*/15 * * * *", # Co 15 minut
]export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
const triggerTime = controller.scheduledTime
switch (controller.cron) {
case '0 * * * *':
// Hourly task
await syncData(env)
break
case '0 0 * * *':
// Daily cleanup
await cleanupOldData(env)
break
default:
console.log(`Unknown cron: ${controller.cron}`)
}
},
}Bezpieczeństwo
WAF Rules
// Cloudflare Dashboard lub API
// Custom WAF rules można konfigurować przez Dashboard
// W Worker możesz sprawdzić CF headers
export default {
async fetch(request: Request): Promise<Response> {
// Sprawdź Bot Score
const botScore = request.cf?.botManagement?.score
if (botScore && botScore < 30) {
return new Response('Bot detected', { status: 403 })
}
// Sprawdź kraj
const country = request.cf?.country
if (country === 'XX') {
return new Response('Access denied', { status: 403 })
}
// Rate limiting (z KV lub Durable Objects)
const ip = request.headers.get('CF-Connecting-IP')
// ... implementacja rate limiting
return new Response('OK')
},
}Rate Limiting z KV
async function rateLimit(
kv: KVNamespace,
ip: string,
limit = 100,
window = 60
): Promise<boolean> {
const key = `rate:${ip}`
const current = await kv.get(key, { type: 'json' }) as {
count: number
resetAt: number
} | null
const now = Date.now()
if (!current || current.resetAt < now) {
await kv.put(key, JSON.stringify({
count: 1,
resetAt: now + window * 1000,
}), { expirationTtl: window })
return true
}
if (current.count >= limit) {
return false
}
await kv.put(key, JSON.stringify({
count: current.count + 1,
resetAt: current.resetAt,
}), { expirationTtl: window })
return true
}
// Użycie
const allowed = await rateLimit(env.KV, ip, 100, 60)
if (!allowed) {
return new Response('Rate limited', { status: 429 })
}Cennik
Free Tier
- Workers: 100,000 req/day
- KV: 100,000 reads/day, 1,000 writes/day
- D1: 5M rows read/day, 100K rows written/day
- R2: 10GB storage, no egress fees
- Pages: Unlimited sites, 500 builds/month
Workers Paid ($5/month minimum)
- Requests: $0.50 per million after 10M
- CPU time: Included in request price
- KV: $0.50 per million reads
- D1: $0.75 per million rows read
- R2: $0.015/GB storage, $0 egress
Porównanie z AWS
| Usługa | Cloudflare | AWS |
|---|---|---|
| Compute (1M req) | $0.50 | $0.20 + duration |
| Storage (10GB) | $0.15/mo | $0.23/mo |
| Egress (100GB) | $0 | $9.00 |
| Database reads (1M) | $0.75 | $1.25 (DynamoDB) |
Best Practices
1. Używaj Hono dla API
import { Hono } from 'hono'
const app = new Hono()
// Lepszy routing, middleware, error handling2. Cache agresywnie
const cacheKey = new Request(request.url, request)
const cache = caches.default
let response = await cache.match(cacheKey)
if (!response) {
response = await handleRequest(request)
ctx.waitUntil(cache.put(cacheKey, response.clone()))
}3. Używaj KV dla session/cache
await env.KV.put(`session:${sessionId}`, JSON.stringify(data), {
expirationTtl: 3600,
})4. D1 dla structured data
// Używaj prepared statements
const stmt = db.prepare('SELECT * FROM users WHERE id = ?')
const user = await stmt.bind(userId).first()5. R2 dla plików
// Zero egress fees!
await env.BUCKET.put(key, file, {
httpMetadata: { contentType: file.type },
})FAQ
Workers vs Lambda@Edge?
Workers są szybsze (zero cold starts), działają w większej liczbie lokalizacji (300+ vs 13), i mają prostszy pricing.
Jak migrować z Vercel?
- Użyj
@cloudflare/next-on-pagesdla Next.js - Lub przepisz API na Workers z Hono
- Przenieś statyczne pliki na Pages
- Skonfiguruj custom domain
Czy D1 jest production-ready?
Tak, od 2024 D1 jest GA (Generally Available). Nadaje się do produkcji, ale dla bardzo dużych baz rozważ Turso lub PlanetScale.
Jak debugować Workers?
# Lokalne logi
wrangler dev --local
# Produkcyjne logi
wrangler tailPodsumowanie
Cloudflare to kompletna platforma edge computing oferująca:
- Workers - Serverless na 300+ lokalizacjach, zero cold starts
- D1 - SQLite na edge
- R2 - Object storage bez egress fees
- KV - Key-value storage
- Pages - Hosting statyczny i SSR
- Bezpieczeństwo - WAF, DDoS, Bot Management
Cloudflare jest idealny dla API wymagających niskiego latency, globalnych aplikacji i optymalizacji kosztów bandwidth.