Astro + Node.js だけでX風Webアプリを作ってみた!

はじめに

近年注目されているフレームワーク「Astro」は、軽量なブログやWebアプリケーションの開発に適していると言われています。

プロダクト開発の形の 1 つとしてパフォーマンス重視の小規模アプリケーションを展開する可能性を考慮しAstroを検討いたしました。

本記事ではAstroの基本的な使用感や実装の容易さを確認するため、SNS風のWebアプリを構築し検証を行いました。スタイリングにはTailwind CSSを用い、実用的なUI構成も併せて評価していきます。


作るもの

  • ポスト一覧表示機能
  • ポスト投稿機能

最終的な完成図はこちらです!


使用する技術

  • Astro
  • Node.js
  • TypeScript (optional)
  • Tailwind CSS

プロジェクト構成

astro と Tailwind CSS のインストールが必要なので、下記の手順で進めましょう! astro は npm create を使えばテンプレートの構成を自動で準備してくれます。

  • astro のインストール
    npm create astro@latest astro-sample
    cd astro-sample
  • API を使用できるようにする
    // @ts-check
    import { defineConfig } from 'astro/config';
    
    // https://astro.build/config
    export default defineConfig({
      output: 'server' // ← ここを追加!
    });
  • Tailwind CSS の追加:
    • Tailwind CSS のインストール
      npm install tailwindcss @tailwindcss/vite
    • astro の設定ファイルの編集
      // @ts-check
      import { defineConfig } from "astro/config";
      import tailwindcss from "@tailwindcss/vite";
      // https://astro.build/config
      export default defineConfig({
          output: 'server',
        vite: {
          plugins: [tailwindcss()],
        },
      });
    • css の定義
      @import "tailwindcss";
       
    • 参考:https://tailwindcss.com/docs/installation/framework-guides/astro

ディレクトリ構成

今回は最低限の実装のみ行いますので、下記の構成に書き換えてください。

my-local-x/
├── .astro/ ← astro 側が使用するディレクトリ(人は触らない)
├── .vscode/ ← vscode用の拡張機能など
├── src/
│   ├── pages/
│   │   ├── index.astro ← フロントエンド(ポスト一覧と投稿フォーム)
│   │   └── api/
│   │       └── post.ts ← バックエンド(ポスト投稿用 API )
│   └── styles/
│       └── global.css ← Tailwind CSS の読み込み
├── public/
│   └── favicon.svg
├── astro.config.mjs ← astro の設定ファイル
├── package.json
├── package-lock.json
└── tsconfig.json
 

ポストの管理用 API /src/pages/api/post.ts

下記がポストの取得と追加のためのAPIを定義するコードとなります。

import type { APIRoute } from 'astro';

export type Post = { content: string; createdAt: Date }

/**
 * ポスト一覧
 * - 今回は変数で管理しているが、本来 DB で管理する
 */
let posts: Post[] = [];

/** ポスト一覧を送信 */
export const GET: APIRoute = () => {
  return new Response(JSON.stringify(posts), {
    headers: { 'Content-Type': 'application/json' },
  });
};

/** ポストを追加 */
export const POST: APIRoute = async ({ request }) => {
  // ポストの内容を取得
  const formData = await request.formData();
  const content = formData.get('content')?.toString() ?? '';

  // ポストの内容が空の場合はエラーを返す
  if (!content.trim()) {
    return new Response('Empty content', { status: 400 });
  }

  // ポストを追加
  posts.unshift({
    content,
    createdAt: new Date(),
  });

  // クライアント側にポスト一覧を更新させるために、リダイレクトさせる
  return new Response(null, {
    status: 303,
    headers: { Location: '/' },
  });
};
 

API のエンドポイントは pages 以下のパスとなり、今回だと api/post になります。 その中で、HTTPメソッドの名前を持つ関数をエクスポートできるようです。今回は下記の 2 つの関数を定義しています。

  • GET:ポストの一覧を取得する
  • POST:ポストを投稿する

この辺りは直感的でわかりやすいかと思います。

参考:https://docs.astro.build/ja/guides/endpoints/

ここからコードの内容を詳しく見ていきたいと思います。

今回はこの変数にポストの情報を貯めていきます。本来は DB で管理すべきですが、今回は変数で管理します。サーバーを再起動したらリセットされてしまうので、気を付けましょう。

let posts: Post[] = [];
 

次に、GETメソッドでポストの内容を送信できるようにしましょう。レスポンスには組み込みのResponseオブジェクトが使用できるようなので、こちらを使用します。

/** ポスト一覧を送信 */
export const GET: APIRoute = () => {
  return new Response(JSON.stringify(posts), {
    headers: { 'Content-Type': 'application/json' },
  });
};
 

POSTメソッドでは、引数からパラメータを受け取れます。こちらの request も組み込みのRequestオブジェクトが使用されているようなので、独自の学習コストがあまりかからずいいですね。

/** ポストを追加 */
export const POST: APIRoute = async ({ request }) => {
  // ポストの内容を取得
  const formData = await request.formData();
  const content = formData.get('content')?.toString() ?? '';
 

ポストの情報を配列に追加して保存します。新しい順にするために unshift を使用してます!

// ポストを追加
posts.unshift({
  content,
  createdAt: new Date(),
});
 

配列に追加しただけではクライアント側に表示するポスト一覧は更新されないので、リダイレクトさせることでポスト一覧を再取得させます!

/** ポストを追加 */
export const POST: APIRoute = async ({ request }) => {
  // ポストの内容を取得
  const formData = await request.formData();
  const content = formData.get('content')?.toString() ?? '';
 

バックエンドはざっくりこんな感じです。下記のメリットを感じました!

  • ファイルのパスがエンドポイントになっていてわかりやすい!
  • 関数名がHTTPメソッド名だからわかりやすい!
  • 通信のインターフェースが組み込みオブジェクトを使用しているので、学習コストが低い!

トップページ /src/pages/index.astro

下記がフロントエンドのポスト一覧表示機能とポスト投稿フォームのコードになります!

---
import "../styles/global.css";
import type { Post } from "./api/post";

const res = await fetch("http://localhost:4321/api/post");
const posts: Post[] = await res.json();
---

<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>X 風 Web アプリ</title>
  </head>
  <body class="max-w-xl mx-auto p-4">
    <h1 class="text-2xl font-bold mb-4">X 風 Web アプリ</h1>

    <form action="/api/post" method="POST" class="mb-4">
      <textarea
        name="content"
        rows="3"
        class="w-full border p-2 rounded"
        placeholder="いま何してる?"></textarea>
      <button
        type="submit"
        class="bg-blue-500 text-white px-4 py-2 mt-2 rounded">投稿</button
      >
    </form>

    <ul>
      {
        posts.map((post) => (
          <li class="border-b py-2">
            <p>{post.content}</p>
            <p class="text-sm text-gray-500">
              {new Date(post.createdAt).toLocaleString()}
            </p>
          </li>
        ))
      }
    </ul>
  </body>
</html>
 

Astro ファイルの基本の記法としては、 --- の間にJS(TS)を記載し、その下にJSXライクなHTMLテンプレートを実装できます。Astro ではサーバー側でHTMLを組み立てるため、--- の間に書くプログラムとHTMLテンプレートの中の式はサーバー側で実行されると覚えておきましょう。 クライアントサイドで実行したいスクリプトは <script> タグを使用してHTMLテンプレート内へ埋め込みましょう。

参考:https://docs.astro.build/ja/reference/astro-syntax/

それでは上のコードについて詳しく見ていきます。

上部のプログラム部分に関しては、基本的にTypeScriptを使用して実装することができます。ここでは組み込みの fetch 関数を使用してポスト一覧を取得するコードを書いています。

import "../styles/global.css";
import type { Post } from "./api/post";

const res = await fetch("http://localhost:4321/api/post");
const posts: Post[] = await res.json();
 

フォームの送信に関しては、HTMLの記法そのまま使えばいいので、スクリプトを書く必要がなく便利ですね!

<form action="/api/post" method="POST" class="mb-4">
      <textarea
        name="content"
        rows="3"
        class="w-full border p-2 rounded"
        placeholder="いま何してる?"></textarea>
      <button
        type="submit"
        class="bg-blue-500 text-white px-4 py-2 mt-2 rounded">投稿</button
      >
    </form>
 

参考:https://docs.astro.build/ja/recipes/build-forms-api/

投稿一覧表示に関しては、JSXライクな記法が使えるので便利です。上部に記載したプログラムで定義した変数を使ってあげましょう!

<ul>
      {
        posts.map((post) => (
          <li class="border-b py-2">
            <p>{post.content}</p>
            <p class="text-sm text-gray-500">
              {new Date(post.createdAt).toLocaleString()}
            </p>
          </li>
        ))
      }
    </ul>
 

フロントエンドも全体的に簡潔に記載できるのが、いいですね! 下記にAstroのフロントエンドの特徴をまとめておきます!

  • JSXライクなテンプレートエンジン!(Reactに慣れている人は便利ですよね!)
  • スクリプトを使用しなくてよいフォーム!

実行方法

npm run dev
 

http://localhost:4321 にアクセスして動作を確認してみましょう!


今後の拡張案

Astro 単体でもいろんなことがシンプルにできて、ちょっとしたWebアプリが簡単に実装できそうですが、他のサービス等を使って拡張して複雑なこともできたりしそうです。

  • DB を接続してデータの管理をする!
    • Astro DB というものも用意されているので、簡単に導入できそうですね!
  • ReactやVueなどフロントエンドフレームワークのコンポーネントを使える!
    • React用のライブラリなど使えて便利そうですよね!

参考:https://docs.astro.build/ja/guides/framework-components/


おわりに

今回の検証を通じて、Astroは静的サイトの構築に特化したフレームワークでありながら、APIルートの活用によって、一定のインタラクティブ性を持ったWebアプリケーションの実装にも対応可能であることが確認できました。 想定していたよりも柔軟性が高く、拡張性もあるため、要件次第ではプロダクト開発への導入も現実的な選択肢となり得ます。

今後はより複雑なユースケースに対しても、適用可能性を評価しながら、フロントエンド技術スタックの最適化を行っていきたいと考えています。

ShtockData

お問い合わせフォーム

お問い合わせ項目を選択してください