Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik19 min czytania

Cloudflare

Cloudflare to kompleksowa platforma edge computing oferująca CDN, Workers, Pages, D1, R2, bezpieczeństwo i DNS dla nowoczesnych aplikacji.

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

  1. Globalny edge - 300+ lokalizacji, < 50ms latency globalnie
  2. Zero cold starts - Workers uruchamiają się natychmiast
  3. Hojny free tier - 100k req/day na Workers, 10GB R2
  4. Zintegrowany ekosystem - Workers + D1 + R2 + KV współpracują seamlessly
  5. Bezpieczeństwo - WAF, DDoS protection, Bot Management wbudowane
  6. Developer Experience - Wrangler CLI, lokalne środowisko dev

Cloudflare vs AWS/Vercel/Netlify

CechaCloudflareAWS Lambda@EdgeVercelNetlify
Edge locations300+13 (Lambda@Edge)~20~20
Cold start0ms100-500ms~50ms~100ms
Free tier100k req/dayPłatne100k req/mo125k req/mo
DatabaseD1 (SQLite)DynamoDBPostgres (Neon)Brak natywnej
StorageR2 (no egress)S3 (egress fee)BlobBrak
Pricing modelPay-per-requestPay-per-durationBandwidthBandwidth

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

Code
Bash
# 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 hono

Podstawowy Worker

TSsrc/index.ts
TypeScript
// 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
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

TSsrc/types.ts
TypeScript
// 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)

Code
Bash
npm create cloudflare@latest my-api -- --template hono
TSsrc/index.ts
TypeScript
// 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 app

Lokalny development

Code
Bash
# 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 8787

Deploy

Code
Bash
# Deploy do Cloudflare
wrangler deploy

# Deploy do konkretnego środowiska
wrangler deploy --env production

# Preview deploy
wrangler deploy --env preview

D1 - SQLite na Edge

Tworzenie bazy D1

Code
Bash
# Utwórz bazę
wrangler d1 create my-database

# Skopiuj ID do wrangler.toml
# database_id = "xxxxx-xxxx-xxxx-xxxx"

Schema i migracje

Code
SQL
-- 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);
Code
Bash
# Uruchom migracje
wrangler d1 migrations apply my-database

# Lokalnie
wrangler d1 migrations apply my-database --local

Queries w D1

Code
TypeScript
// 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

Code
Bash
npm install drizzle-orm
npm install -D drizzle-kit
TSdb/schema.ts
TypeScript
// 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),
})
TSdb/index.ts
TypeScript
// 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

Code
Bash
# Utwórz KV namespace
wrangler kv:namespace create "MY_KV"

# Preview namespace
wrangler kv:namespace create "MY_KV" --preview

Operacje KV

Code
TypeScript
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

Code
TypeScript
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

Code
Bash
# Utwórz bucket
wrangler r2 bucket create my-bucket

Operacje R2

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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 app

Cloudflare Pages

Deploy statycznej strony

Code
Bash
# Z folderu dist
wrangler pages deploy ./dist

# Z GitHub (automatyczne deploye)
# Połącz repo w Cloudflare Dashboard

Pages z Next.js

Code
Bash
# Dodaj adapter
npm install @cloudflare/next-on-pages

# Build
npx @cloudflare/next-on-pages

# Deploy
wrangler pages deploy .vercel/output/static
JSnext.config.js
JavaScript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    runtime: 'edge',
  },
}

module.exports = nextConfig

Pages Functions (API routes)

Code
TEXT
project/
├── functions/
│   ├── api/
│   │   ├── users.ts        # /api/users
│   │   └── users/
│   │       └── [id].ts     # /api/users/:id
│   └── _middleware.ts      # Global middleware
├── public/
└── dist/
TSfunctions/api/users.ts
TypeScript
// 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 })
}
TSfunctions/_middleware.ts
TypeScript
// 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

TSsrc/counter.ts
TypeScript
// 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

Code
TypeScript
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

Code
Bash
wrangler queues create my-queue

Producer (wysyłanie)

Code
TypeScript
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)

Code
TypeScript
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
TOML
# wrangler.toml
[triggers]
crons = [
  "0 * * * *",      # Co godzinę
  "0 0 * * *",      # Codziennie o północy
  "*/15 * * * *",   # Co 15 minut
]
Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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ługaCloudflareAWS
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

Code
TypeScript
import { Hono } from 'hono'
const app = new Hono()
// Lepszy routing, middleware, error handling

2. Cache agresywnie

Code
TypeScript
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

Code
TypeScript
await env.KV.put(`session:${sessionId}`, JSON.stringify(data), {
  expirationTtl: 3600,
})

4. D1 dla structured data

Code
TypeScript
// 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

Code
TypeScript
// 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?

  1. Użyj @cloudflare/next-on-pages dla Next.js
  2. Lub przepisz API na Workers z Hono
  3. Przenieś statyczne pliki na Pages
  4. 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?

Code
Bash
# Lokalne logi
wrangler dev --local

# Produkcyjne logi
wrangler tail

Podsumowanie

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.