Val Town:サーバーレスTypeScriptスクリプト実行プラットフォーム完全ガイド


Val Townは、TypeScriptコードをブラウザで書いて即座にクラウドで実行できる革新的なサーバーレスプラットフォームです。関数をURLとして公開したり、Cronジョブとして定期実行したり、メールを送信したりと、様々な自動化タスクを無料で実装できます。

Val Townとは

Val Townは「Val」と呼ばれる小さなTypeScript/JavaScriptコードスニペットをクラウドで実行するプラットフォームです。

主な特徴

  • 即座に実行: コードを書いたら即座にデプロイ、URLで公開
  • 無料プラン充実: 月10,000実行まで無料
  • TypeScript完全サポート: 型安全なコード記述
  • 組み込みライブラリ: fetch、SQLite、メール送信などが標準装備
  • 公開・非公開選択可能: ValをPublicまたはPrivateで管理
  • バージョン管理: Valの履歴を自動保存

ユースケース

  • APIエンドポイントの作成
  • Cronジョブによる定期実行
  • Webhook受信・処理
  • データスクレイピング
  • 通知・アラート送信
  • 簡易データベース操作

アカウント作成と基本操作

1. アカウント作成

1. https://val.town にアクセス
2. GitHubアカウントでログイン
3. ユーザー名を設定

2. 最初のVal作成

// ボタン「New Val」をクリック → 「HTTP」を選択

export default async function handler(req: Request): Promise<Response> {
  return new Response("Hello, Val Town!");
}

保存すると即座にURLが発行されます。

https://your-username-valname.web.val.run

HTTP API作成

基本的なGET API

export default async function getUserAPI(req: Request): Promise<Response> {
  const url = new URL(req.url);
  const userId = url.searchParams.get("id");

  if (!userId) {
    return Response.json(
      { error: "User ID is required" },
      { status: 400 }
    );
  }

  // ダミーユーザーデータ
  const user = {
    id: userId,
    name: "Taro Yamada",
    email: "taro@example.com",
  };

  return Response.json(user);
}

使い方:

https://your-username-getUserAPI.web.val.run?id=123

POST API(フォーム処理)

export default async function createUserAPI(req: Request): Promise<Response> {
  if (req.method !== "POST") {
    return Response.json(
      { error: "Method not allowed" },
      { status: 405 }
    );
  }

  const body = await req.json();
  const { name, email } = body;

  if (!name || !email) {
    return Response.json(
      { error: "Name and email are required" },
      { status: 400 }
    );
  }

  // データベース保存処理(後述)
  const userId = Math.random().toString(36).substr(2, 9);

  return Response.json({
    success: true,
    user: { id: userId, name, email },
  }, { status: 201 });
}

curlでテスト:

curl -X POST https://your-username-createUserAPI.web.val.run \
  -H "Content-Type: application/json" \
  -d '{"name":"Taro","email":"taro@example.com"}'

CORS対応API

export default async function corsAPI(req: Request): Promise<Response> {
  const headers = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
  };

  // プリフライトリクエスト対応
  if (req.method === "OPTIONS") {
    return new Response(null, { headers });
  }

  const data = { message: "CORS enabled API" };
  return Response.json(data, { headers });
}

Cronジョブ(定期実行)

毎日実行するジョブ

import { email } from "https://esm.town/v/std/email";

export default async function dailyReport() {
  const today = new Date().toISOString().split('T')[0];

  // データ集計処理
  const stats = await fetchDailyStats();

  // メール送信
  await email({
    subject: `Daily Report - ${today}`,
    text: `
      Total Users: ${stats.users}
      Total Revenue: $${stats.revenue}
      New Signups: ${stats.signups}
    `,
  });

  console.log("Daily report sent:", today);
}

async function fetchDailyStats() {
  // 実際のデータ取得ロジック
  return {
    users: 1234,
    revenue: 5678,
    signups: 42,
  };
}

Cron設定:

  1. Valの設定画面を開く
  2. 「Schedule」タブを選択
  3. 0 9 * * * (毎日9時)を設定

5分ごとに実行するモニタリング

export default async function monitorWebsite() {
  const url = "https://example.com";

  try {
    const response = await fetch(url, { method: "HEAD" });

    if (!response.ok) {
      await sendAlert(`Website down! Status: ${response.status}`);
    }
  } catch (error) {
    await sendAlert(`Website unreachable: ${error.message}`);
  }
}

async function sendAlert(message: string) {
  // Slack通知
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: message }),
  });
}

Cron設定: */5 * * * * (5分ごと)

メール送信

基本的なメール送信

import { email } from "https://esm.town/v/std/email";

export default async function sendWelcomeEmail(req: Request): Promise<Response> {
  const { userEmail, userName } = await req.json();

  await email({
    to: userEmail,
    subject: "Welcome to our service!",
    text: `Hi ${userName},\n\nWelcome aboard!`,
    html: `<h1>Hi ${userName}</h1><p>Welcome aboard!</p>`,
  });

  return Response.json({ success: true });
}

HTMLメールテンプレート

import { email } from "https://esm.town/v/std/email";

export default async function sendInvoice(req: Request): Promise<Response> {
  const { userEmail, invoiceData } = await req.json();

  const html = `
    <!DOCTYPE html>
    <html>
      <head>
        <style>
          body { font-family: Arial, sans-serif; }
          table { border-collapse: collapse; width: 100%; }
          th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
          th { background-color: #4CAF50; color: white; }
        </style>
      </head>
      <body>
        <h2>Invoice #${invoiceData.id}</h2>
        <table>
          <tr><th>Item</th><th>Price</th></tr>
          ${invoiceData.items.map(item =>
            `<tr><td>${item.name}</td><td>$${item.price}</td></tr>`
          ).join('')}
        </table>
        <p><strong>Total: $${invoiceData.total}</strong></p>
      </body>
    </html>
  `;

  await email({ to: userEmail, subject: `Invoice #${invoiceData.id}`, html });

  return Response.json({ success: true });
}

SQLiteデータベース

Val TownにはSQLiteが組み込まれています。

データ保存

import { sqlite } from "https://esm.town/v/std/sqlite";

export default async function saveUserAPI(req: Request): Promise<Response> {
  const { name, email } = await req.json();

  // テーブル作成(初回のみ)
  await sqlite.execute(`
    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
    )
  `);

  // データ挿入
  await sqlite.execute({
    sql: "INSERT INTO users (name, email) VALUES (?, ?)",
    args: [name, email],
  });

  return Response.json({ success: true });
}

データ取得

import { sqlite } from "https://esm.town/v/std/sqlite";

export default async function getUsersAPI(req: Request): Promise<Response> {
  const result = await sqlite.execute("SELECT * FROM users ORDER BY id DESC LIMIT 10");

  return Response.json({
    users: result.rows,
    total: result.rows.length,
  });
}

データ検索

import { sqlite } from "https://esm.town/v/std/sqlite";

export default async function searchUsersAPI(req: Request): Promise<Response> {
  const url = new URL(req.url);
  const query = url.searchParams.get("q") || "";

  const result = await sqlite.execute({
    sql: "SELECT * FROM users WHERE name LIKE ? OR email LIKE ?",
    args: [`%${query}%`, `%${query}%`],
  });

  return Response.json({ users: result.rows });
}

Webhook処理

GitHub Webhook

export default async function githubWebhook(req: Request): Promise<Response> {
  const payload = await req.json();
  const event = req.headers.get("X-GitHub-Event");

  if (event === "push") {
    const { repository, commits } = payload;

    console.log(`New push to ${repository.name}`);
    console.log(`${commits.length} commits`);

    // Slack通知
    await notifySlack(`New push to ${repository.name}: ${commits.length} commits`);
  }

  return Response.json({ received: true });
}

async function notifySlack(message: string) {
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: message }),
  });
}

Stripe Webhook(決済通知)

export default async function stripeWebhook(req: Request): Promise<Response> {
  const payload = await req.json();
  const { type, data } = payload;

  switch (type) {
    case "payment_intent.succeeded":
      await handlePaymentSuccess(data.object);
      break;
    case "payment_intent.payment_failed":
      await handlePaymentFailed(data.object);
      break;
  }

  return Response.json({ received: true });
}

async function handlePaymentSuccess(paymentIntent: any) {
  console.log("Payment succeeded:", paymentIntent.id);
  // データベース更新、領収書送信など
}

async function handlePaymentFailed(paymentIntent: any) {
  console.log("Payment failed:", paymentIntent.id);
  // 管理者通知など
}

環境変数の使用

環境変数設定

1. Valの設定画面を開く
2. 「Secrets」タブを選択
3. キーと値を入力して保存

コード内で使用

export default async function useEnvAPI(req: Request): Promise<Response> {
  const apiKey = process.env.EXTERNAL_API_KEY;
  const dbUrl = process.env.DATABASE_URL;

  // 外部API呼び出し
  const response = await fetch("https://api.example.com/data", {
    headers: { "Authorization": `Bearer ${apiKey}` },
  });

  const data = await response.json();

  return Response.json(data);
}

実用例:URLショートナー

import { sqlite } from "https://esm.town/v/std/sqlite";

export default async function urlShortener(req: Request): Promise<Response> {
  await initDatabase();

  const url = new URL(req.url);
  const path = url.pathname;

  // リダイレクト処理
  if (path.startsWith("/r/")) {
    const code = path.replace("/r/", "");
    return await redirect(code);
  }

  // URL短縮API
  if (req.method === "POST") {
    const { url: targetUrl } = await req.json();
    const code = await createShortUrl(targetUrl);
    return Response.json({ shortUrl: `/r/${code}`, code });
  }

  return Response.json({ error: "Invalid request" }, { status: 400 });
}

async function initDatabase() {
  await sqlite.execute(`
    CREATE TABLE IF NOT EXISTS urls (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      code TEXT UNIQUE NOT NULL,
      url TEXT NOT NULL,
      clicks INTEGER DEFAULT 0,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);
}

async function createShortUrl(url: string): Promise<string> {
  const code = Math.random().toString(36).substr(2, 6);
  await sqlite.execute({
    sql: "INSERT INTO urls (code, url) VALUES (?, ?)",
    args: [code, url],
  });
  return code;
}

async function redirect(code: string): Promise<Response> {
  const result = await sqlite.execute({
    sql: "SELECT url FROM urls WHERE code = ?",
    args: [code],
  });

  if (result.rows.length === 0) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  // クリック数更新
  await sqlite.execute({
    sql: "UPDATE urls SET clicks = clicks + 1 WHERE code = ?",
    args: [code],
  });

  return Response.redirect(result.rows[0].url, 302);
}

使い方:

# URL短縮
curl -X POST https://your-username-urlShortener.web.val.run \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/very/long/url"}'

# レスポンス: {"shortUrl":"/r/abc123","code":"abc123"}

# アクセス
curl https://your-username-urlShortener.web.val.run/r/abc123

料金プラン

Free Plan

  • 月10,000実行
  • Public Valsは無制限閲覧可能
  • 基本機能すべて利用可能

Pro Plan ($10/月)

  • 月100万実行
  • Private Vals
  • カスタムドメイン
  • 優先サポート

まとめ

Val Townは以下の点で優れています。

メリット:

  • 無料で始められる
  • TypeScript完全サポート
  • デプロイが即座
  • Cronジョブ標準装備
  • SQLiteデータベース内蔵
  • メール送信が簡単

適したユースケース:

  • 簡易API開発
  • 定期実行タスク
  • Webhook処理
  • プロトタイピング
  • 個人プロジェクト

注意点:

  • 大規模トラフィックには不向き
  • 実行時間制限あり
  • ストレージ容量制限

小規模なAPIやCronジョブ、個人プロジェクトには最適なプラットフォームです。ぜひ試してみてください。

参考リンク: