【第8回】サイトから閲覧や投稿ができるアプリケーションを作ろう(投稿編)

今回は投稿をサイト上からできるようにして、データベースに保存するところまでやっていきます。

Homeブログ一覧【第8回】サイトから閲覧や投稿ができるアプリケーションを作ろう(投稿編)

おさらい

前回は、サイト上からデータベースに保存されているデータを取得して表示するところまでやりました。

まずはHTTPというプロトコルの概要とContent-Typeというヘッダーについて説明しました。 このContent-Typeは、サーバーからクライアントに送信するデータの種類を表すもので、

  • text/html : HTMLとして解釈される
  • text/plain : テキストとして解釈される
  • application/json : JSONとして解釈される

のように、通信で受け取った(送った)データの種類を表すものです。 これを間違うと、ブラウザが正しくデータを解釈できないので、正しく設定する必要があります。

そしてそのあと、Content-Typetext/htmlであれば別にHTMLファイルを作らずとも、普通のHTMLが書かれた文字列を送信することで、ブラウザがHTMLとして解釈してくれることを確認しました。

その挙動を利用して、実際にサーバー(前回はHonoを利用しました)からデータを取得して、サーバー上でHTML(文字列)を生成して、それをクライアントに送信するということをやりました。

この操作をサーバーサイドレンダリングもしくはSSRと呼びました。 SSRをすると簡単にサーバーからデータを取得して、それをHTMLに埋め込んで表示することができます。 しかし、SSRはサーバーの負荷が高くなるという欠点がありました。

今回やること

前回データベースに保存されているデータを取得してSSRして表示するところまでやりました。 でも、データベースに保存されているデータをサイト上から追加投稿したいですよね? 今回は、サイト上からデータベースに保存されているデータを追加投稿できるようにしていきます。

今回も前回の続きになりますので、前回の記事を終わらせてから進めてください。

最小のフォーム

今回は、一番ベーシックな「フォームを使う」方法で投稿を実装していきます。

フォームを使う方法は、サーバーにデータを送信するための方法の一つです。

フォームを使う方法は、HTMLの<form>タグを使って実装します。

(これは例なのでプロジェクトにコピーしなくて大丈夫です)

<form action="/post" method="POST">
  <input type="text" name="title" />
  <input type="text" name="content" />
  <button type="submit">投稿</button>
</form>

フォーム

このように、<form>タグのaction属性には、データを送信する先のURLを指定します。たとえば、/postでサーバーが受け取れるようにしてあるなら、action="/post"とします。

POSTというのは、HTTPのメソッドの一つで、データを送信するときに使います。 Honoでは、GETメソッドの通信にapp.getを使いましたが、POSTメソッドの通信にはapp.postを使うことになります。

そして、<input>タグのtype属性には、入力するデータの種類を指定します。今回は、type="text"にしています。 他にも、type="password"とするとパスワードを入力できたり、type="number"とすると数字を入力できたりします。

そして、<input>タグのname属性には、送信するデータの名前を指定します。 今回は、name="title"name="content"にしています。

最後に、<button>タグのtype属性には、ボタンの種類を指定します。今回は、type="submit"にしています。 type="submit"にすると、ボタンを押したときにフォームのデータを送信することができます。

より実践的なフォーム

ラベルをつける

最小のフォームを作ることができたので、より実践的なフォームを作っていきます。 先ほどの画像のような感じではどの入力フィールドに何を入力すればいいのかわからないのですよね。 これでは使いづらいので、入力フィールドの上にラベルをつけてあげましょう。

<form action="/post" method="POST">
  <label for="title">タイトル</label>
  <input type="text" name="title" id="title" />
  <label for="content">内容</label>
  <input type="text" name="content" id="content" />
  <button type="submit">投稿</button>
</form>

<label>タグのfor属性には、そのラベルがついている入力フィールドのid属性の値を指定します。 <input>タグのid属性には、その入力フィールドの名前を指定します。 こうすることで、入力フィールドとラベルが紐づけられます。

テキストエリアを使う

次に、入力フィールドを使うのではなく、テキストエリアを使ってみましょう。

<form action="/post" method="POST">
  <label for="title">タイトル</label>
  <input type="text" name="title" id="title" />
  <label for="content">内容</label>
  <textarea name="content" id="content" rows="10"></textarea>
  <button type="submit">投稿</button>
</form>

<textarea><input type="text">とほぼ同じ使い方ができますが、<textarea>は複数行のテキストを入力することができます。 <textarea>タグのrows属性には、テキストエリアの行数を指定します。これは最大行数ではなく初期表示の行数です。

プレイスホルダーを使う

次に、入力フィールドにプレイスホルダーをつけてみましょう。

Twitterのフォーム

Twitterのフォームもプレイスホルダーを使っているので、見たことがある人も多いと思います。これです。

<form action="/post" method="POST">
  <label for="title">タイトル</label>
  <input type="text" name="title" id="title" placeholder="タイトル" />
  <label for="content">内容</label>
  <textarea name="content" id="content" placeholder="内容" rows="10"></textarea>
  <button type="submit">投稿</button>
</form>
<style>
  input,
  textarea {
    width: 100%;
    margin-bottom: 16px;
  }
</style>

フルカスタムフォーム

ちょっと見栄えが悪いので、CSSでちょっとだけ見栄えを良くしています。

<input>タグと<textarea>タグのplaceholder属性には、プレイスホルダーの文字列を指定します。 プレイスホルダーとは、入力フィールドに何も入力されていないときに表示される文字列のことです。

プロジェクトの整理

投稿機能を作る前にHTML文字列を別ファイルに分けましょう。 前回作ったindex.jsの中のHTML変数などの埋め込み文字列を別ファイルに分けて、index.jsから読み込むようにします。

ではtemplates.jsというファイルを作って、そこにHTML文字列を書いていきます。 ちなみに、今後こういったHTML文字列をテンプレートビューと呼びます。

templates.js
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>これはただの文字列です</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    ${body}
</body>
</html>
`;

const TWEET_LIST_VIEW = (tweets) => `
<h1 class="title">ツイート一覧</h1>
<div class="tweet-list">
    ${tweets
      .map((tweet) => `<div class="tweet">${tweet.content}</div>`)
      .join("\n")}
</div>
`;

module.exports = {
  HTML,
  TWEET_LIST_VIEW,
};

index.jsはこのように変えます。 HTML変数が削除されているので注意してください。

index.js
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const templates = require("./templates");
const { serve } = require("@hono/node-server");
const { serveStatic } = require("@hono/node-server/serve-static");
const { Hono } = require("hono");

const db = new sqlite3.Database("database.db");

db.serialize(() => {
    db.run(queries.Tweets.createTable);
    db.run(queries.Users.createTable);

    db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
    db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
    db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');

    db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
    db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
    db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});

const app = new Hono();

app.get("/", async (c) => {
  const tweets = await new Promise((resolve) => {
      db.all(queries.Tweets.findAll, (err, rows) => {
          resolve(rows);
      });
  });

  const tweetList = templates.TWEET_LIST_VIEW(tweets);

  const response = templates.HTML(tweetList);

  return c.html(response);
});

app.use("/static/*", serveStatic({ root: "./" }));

serve(app);

process.stdin.on("data", (data) => {
  if (data.toString().trim() === "q") {
    db.close();
    process.exit();
  }
});

こんな感じで、HTML変数をtemplates.jsに移動させて、index.jsから読み込むようにしました。 こうすることで、HTMLを組み立てるという責務をindex.jsからtemplates.jsに移動させることができました。

この修正は動作を変えず、コードを整理するという変更なので、リファクタリングと呼びます。

ではコミットしましょう。

git add .
git commit -m "HTML文字列を別ファイルに分けてリファクタリングした"

ユーザー登録ができるようにする

ユーザー登録のためのフォームを作る

まずは、ユーザー登録のためのフォームを作ります。

templates.js
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>これはただの文字列です</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    ${body}
</body>
</html>
`;

const TWEET_LIST_VIEW = (tweets) => `
<h1 class="title">ツイート一覧</h1>
<div class="tweet-list">
    ${tweets
      .map((tweet) => `<div class="tweet">${tweet.content}</div>`)
      .join("\n")}
</div>
`;

const USER_REGISTER_FORM_VIEW = () => `
<h1 class="title">ユーザー登録</h1>
<form action="/user/register" method="POST">
    <label for="name">名前</label>
    <input type="text" name="name" id="name" />
    <label for="email">メールアドレス</label>
    <input type="email" name="email" id="email" />
    <button type="submit">登録</button>
</form>
`;

module.exports = {
    HTML,
    TWEET_LIST_VIEW,
    USER_REGISTER_FORM_VIEW,
};

これで、ユーザー登録のためのフォームが作れました。 あとはこれをindex.jsから呼び出すだけです。

index.js
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const templates = require("./template");
const { serve } = require("@hono/node-server");
const { serveStatic } = require("@hono/node-server/serve-static");
const { Hono } = require("hono");

const db = new sqlite3.Database("database.db");

db.serialize(() => {
    db.run(queries.Tweets.createTable);
    db.run(queries.Users.createTable);

    db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
    db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
    db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');

    db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
    db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
    db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});

const app = new Hono();

app.get("/", async (c) => {
  const tweets = await new Promise((resolve) => {
      db.all(queries.Tweets.findAll, (err, rows) => {
          resolve(rows);
      });
  });

  const tweetList = templates.TWEET_LIST_VIEW(tweets);

  const response = templates.HTML(tweetList);

  return c.html(response);
});

app.get("/user/register", async (c) => {
    const registerForm = templates.USER_REGISTER_FORM_VIEW();

    const response = templates.HTML(registerForm);

    return c.html(response);
});

app.post("/user/register", async (c) => {
    const body = await c.req.parseBody();
    const now = new Date().toISOString();

    const userID = await new Promise((resolve) => {
        db.run(queries.Users.create, body.name, body.email, now, function(err) {
            resolve(this.lastID);
        });
    });

    return c.redirect(`/user/${userID}`);
});

app.use("/static/*", serveStatic({ root: "./" }));

serve(app);

process.stdin.on("data", (data) => {
  if (data.toString().trim() === "q") {
    db.close();
    process.exit();
  }
});

これで/user/registerにアクセスすると、ユーザー登録のためのフォームが表示されます。 そして今回新たに/user/registerPOSTメソッドでアクセスすると、ユーザー登録ができるようになりました。

なのでUSER_REGISTER_FORM_VIEWで作ったフォームのaction属性には/user/registerを指定しています。

parseBodyというのは、Formから送信されたデータを解釈するための関数です。 これを使うことで、nameemailといったデータを取得することができます。

redirectというのは、リダイレクトをするための関数です。 リダイレクトとは、サーバーからクライアントに対して、別のURLにアクセスするように指示することです。 今回は、ユーザー登録が完了したら、そのユーザーのツイート一覧を表示するようにしています。 (このあとユーザーのツイート一覧を表示する処理を書きますので、今は404エラーが表示されます

できたら、/user/registerにアクセスして、ユーザー登録ができるか確認してみましょう。 まだユーザー登録がされているかどうかはサイト上では確認できませんが、データベースには登録されているので、確認してみましょう。

sqlite3 database.db
SELECT * FROM users;

ちゃんとユーザー登録ができていることが確認できました。

ではコミットしましょう。

git add .
git commit -m "ユーザー登録のためのフォームを作った"

特定のユーザーが投稿したツイート一覧を表示する

ユーザー登録ができるようになったので、ユーザーごとにツイート一覧を表示できるようにしていきます。 要件は

  • /user/:idにアクセスすると、そのユーザーが投稿したツイート一覧が表示される
  • もし、そのユーザーが存在しない場合は、404エラーが表示される

という感じにしてみましょう。

まずは、templates.jsにユーザーのツイート一覧を表示するための関数を追加します。

templates.js:
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>これはただの文字列です</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    ${body}
</body>
</html>
`;

const TWEET_LIST_VIEW = (tweets) => `
<h1 class="title">ツイート一覧</h1>
<div class="tweet-list">
    ${tweets
      .map((tweet) => `<div class="tweet">${tweet.content}</div>`)
      .join("\n")}
</div>
`;

const USER_REGISTER_FORM_VIEW = () => `
<h1 class="title">ユーザー登録</h1>
<form action="/user/register" method="POST">
    <label for="name">名前</label>
    <input type="text" name="name" id="name" />
    <label for="email">メールアドレス</label>
    <input type="email" name="email" id="email" />
    <button type="submit">登録</button>
</form>
`;

const USER_TWEET_LIST_VIEW = (user, tweets) => `
<h1 class="title">${user.name}さんのツイート一覧</h1>
<div class="tweet-list">
    ${tweets
      .map((tweet) => `<div class="tweet">${tweet.content}</div>`)
      .join("\n")}
</div>
`;

module.exports = {
    HTML,
    TWEET_LIST_VIEW,
    USER_REGISTER_FORM_VIEW,
    USER_TWEET_LIST_VIEW,
};

次に特定のユーザーが投稿したツイート一覧を表示するためのSQLをqueries.jsに追加します。

ユーザーIDから全てのツイートを取得するSQLと、ユーザーIDからユーザーを取得するSQLを追加します。

queries.js
const Tweets = {
  createTable: `
      CREATE TABLE IF NOT EXISTS tweets (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          content TEXT NOT NULL,
          user_id INTEGER NOT NULL,
          created_at DATETIME NOT NULL
      );
  `,
  create: `INSERT INTO tweets (content, user_id, created_at) VALUES (?, ?, ?);`,
  findAll: `SELECT * FROM tweets;`,
  findByUserId: `SELECT * FROM tweets WHERE user_id = ?;`,
};

const Users = {
  createTable: `
      CREATE TABLE IF NOT EXISTS users (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          email TEXT NOT NULL,
          created_at DATETIME NOT NULL
      );
  `,
  create: `INSERT INTO users (name, email, created_at) VALUES (?, ?, ?);`,
  findAll: `SELECT * FROM users;`,
  findById: `SELECT * FROM users WHERE id = ?;`,
  findByTweetId: `SELECT * FROM users WHERE id = (SELECT user_id FROM tweets WHERE id = ?);`,
};

module.exports = {
  Tweets,
  Users,
};

そして、index.js/user/:idにアクセスしたときの処理を追加します。

index.js
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const templates = require("./template");
const { serve } = require("@hono/node-server");
const { serveStatic } = require("@hono/node-server/serve-static");
const { Hono } = require("hono");

const db = new sqlite3.Database("database.db");

db.serialize(() => {
    db.run(queries.Tweets.createTable);
    db.run(queries.Users.createTable);

    db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
    db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
    db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');

    db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
    db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
    db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});

const app = new Hono();

app.get("/", async (c) => {
  const tweets = await new Promise((resolve) => {
      db.all(queries.Tweets.findAll, (err, rows) => {
          resolve(rows);
      });
  });

  const tweetList = templates.TWEET_LIST_VIEW(tweets);

  const response = templates.HTML(tweetList);

  return c.html(response);
});

app.get("/user/register", async (c) => {
    const registerForm = templates.USER_REGISTER_FORM_VIEW();

    const response = templates.HTML(registerForm);

    return c.html(response);
});

app.post("/user/register", async (c) => {
    const body = await c.req.parseBody();
    const now = new Date().toISOString();

    const userID = await new Promise((resolve) => {
        db.run(queries.Users.create, body.name, body.email, now, function(err) {
            resolve(this.lastID);
        });
    });

    return c.redirect(`/user/${userID}`);
});

app.get("/user/:id", async (c) => {
    const userId = c.req.param("id");

    const user = await new Promise((resolve) => {
        db.get(queries.Users.findById, userId, (err, row) => {
            resolve(row);
        });
    });

    if (!user) {
        return c.notFound();
    }

    const tweets = await new Promise((resolve) => {
        db.all(queries.Tweets.findByUserId, userId, (err, rows) => {
            resolve(rows);
        });
    });

    const userTweetList = templates.USER_TWEET_LIST_VIEW(user, tweets);

    const response = templates.HTML(userTweetList);

    return c.html(response);
});

app.use("/static/*", serveStatic({ root: "./" }));

serve(app);

process.stdin.on("data", (data) => {
  if (data.toString().trim() === "q") {
    db.close();
    process.exit();
  }
});

これで、/user/:idにアクセスすると、そのユーザーが投稿したツイート一覧が表示されるようになりました。

一度database.dbを削除して、index.jsを実行してみましょう。

すると、/user/3にアクセスすると、ぶどう三郎さんのツイート一覧が表示されますが、/user/4にアクセスすると、404 Not Foundが表示されます。 これは最初のseedで作ったユーザーは3人しかいないからです。 ではサイト上からユーザー登録してみましょう。

投稿後勝手にリダイレクトされて、ユーザー一覧画面が自分が登録した名前で表示されていたら成功です。

ではコミットしましょう。

git add .
git commit -m "特定のユーザーが投稿したツイート一覧を表示するようにした"

投稿フォームをスタイリングする

投稿フォームをスタイリングしていきます。 前作ったstatic/style.cssに以下のCSSを追加してください

static/style.css
body {
  margin: 0;
  background-color: #edf3f6;
}

.title {
  font-size: 60px;
  text-align: center;
}

.tweet-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 768px;
  padding: 16px;
  box-sizing: border-box;
  width: 100%;
  margin: 0 auto;
}

.tweet {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  border-radius: 8px;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
}

form {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20px;
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
}

label {
  font-weight: bold;
  margin-bottom: 5px;
  text-align: left;
  width: 100%;
}

input, textarea, select {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-bottom: 10px;
  width: 100%;
  box-sizing: border-box;
}

button[type="submit"] {
  background-color: #3f8ce4;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 16px;
}

button[type="submit"]:hover {
  background-color: #2a6cb8;
}

これで、投稿フォームがちょっとだけ見栄えが良くなりました。

投稿フォーム

ではコミットしましょう。

git add .
git commit -m "投稿フォームをスタイリングした"

ツイートを投稿できるようにする

ツイートを投稿するためのフォームを作る

ツイートを投稿するためのフォームを作ります。

templates.js:
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>これはただの文字列です</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    ${body}
</body>
</html>
`;

const TWEET_LIST_VIEW = (tweets) => `
<h1 class="title">ツイート一覧</h1>
<div class="tweet-list">
    ${tweets
      .map((tweet) => `<div class="tweet">${tweet.content}</div>`)
      .join("\n")}
</div>
`;

const USER_REGISTER_FORM_VIEW = () => `
<h1 class="title">ユーザー登録</h1>
<form action="/user/register" method="POST">
    <label for="name">名前</label>
    <input type="text" name="name" id="name" />
    <label for="email">メールアドレス</label>
    <input type="email" name="email" id="email" />
    <button type="submit">登録</button>
</form>
`;

const USER_TWEET_LIST_VIEW = (user, tweets) => `
<h1 class="title">${user.name}さんのツイート一覧</h1>
<div class="tweet-list">
    ${tweets
      .map((tweet) => `<div class="tweet">${tweet.content}</div>`)
      .join("\n")}
</div>
`;

const TWEET_FORM_VIEW = (users) => `
<h1 class="title">ツイート</h1>
<form action="/tweet" method="POST">
    <label for="content">内容</label>
    <textarea name="content" id="content" rows="10"></textarea>
    <label for="user_id">ユーザー</label>
    <select name="user_id" id="user_id">
        ${users
          .map((user) => `<option value="${user.id}">${user.name}</option>`)
          .join("\n")}
    </select>
    <button type="submit">投稿</button>
</form>
`;

module.exports = {
    HTML,
    TWEET_LIST_VIEW,
    USER_REGISTER_FORM_VIEW,
    USER_TWEET_LIST_VIEW,
    TWEET_FORM_VIEW,
};

TWEET_FORM_VIEWという関数を追加しました。

実際はここでユーザー認証を実装して他のユーザーが自分になりすまして投稿できないようにする必要がありますが、今回は簡単のためにユーザーを選んで投稿するようにします。

<select>タグの中身は、usersという配列をループして、<option>タグを作っています。 <option>タグのvalue属性には、user.idを指定しています。

次に、index.js/tweetにアクセスしたときの処理を追加します。 ユーザー登録と同じように、POSTメソッドでアクセスするとツイートが投稿されるようにします。

index.js
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const templates = require("./template");
const { serve } = require("@hono/node-server");
const { serveStatic } = require("@hono/node-server/serve-static");
const { Hono } = require("hono");

const db = new sqlite3.Database("database.db");

db.serialize(() => {
    db.run(queries.Tweets.createTable);
    db.run(queries.Users.createTable);

    db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
    db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
    db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');

    db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
    db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
    db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});

const app = new Hono();

app.get("/", async (c) => {
  const tweets = await new Promise((resolve) => {
      db.all(queries.Tweets.findAll, (err, rows) => {
          resolve(rows);
      });
  });

  const tweetList = templates.TWEET_LIST_VIEW(tweets);

  const response = templates.HTML(tweetList);

  return c.html(response);
});

app.get("/user/register", async (c) => {
    const registerForm = templates.USER_REGISTER_FORM_VIEW();

    const response = templates.HTML(registerForm);

    return c.html(response);
});

app.post("/user/register", async (c) => {
    const body = await c.req.parseBody();
    const now = new Date().toISOString();

    const userID = await new Promise((resolve) => {
        db.run(queries.Users.create, body.name, body.email, now, (err) => {
            resolve(this.lastID);
        });
    });

    return c.redirect(`/user/${userID}`);
});

app.get("/user/:id", async (c) => {
    const userId = c.req.param("id");

    const user = await new Promise((resolve) => {
        db.get(queries.Users.findById, userId, (err, row) => {
            resolve(row);
        });
    });

    if (!user) {
        return c.notFound();
    }

    const tweets = await new Promise((resolve) => {
        db.all(queries.Tweets.findByUserId, userId, (err, rows) => {
            resolve(rows);
        });
    });

    const userTweetList = templates.USER_TWEET_LIST_VIEW(user, tweets);

    const response = templates.HTML(userTweetList);

    return c.html(response);
});

app.get("/tweet", async (c) => {
    const users = await new Promise((resolve) => {
        db.all(queries.Users.findAll, (err, rows) => {
            resolve(rows);
        });
    });

    const tweetForm = templates.TWEET_FORM_VIEW(users);

    const response = templates.HTML(tweetForm);

    return c.html(response);
});

app.post("/tweet", async (c) => {
    const body = await c.req.parseBody();
    const now = new Date().toISOString();

    await new Promise((resolve) => {
        db.run(queries.Tweets.create, body.content, body.user_id, now, (err) => {
            resolve();
        });
    });

    return c.redirect("/");
});

app.use("/static/*", serveStatic({ root: "./" }));

serve(app);

process.stdin.on("data", (data) => {
  if (data.toString().trim() === "q") {
    db.close();
    process.exit();
  }
});

これで、/tweetにアクセスすると、ツイートを投稿するためのフォームが表示されます。 そして、POSTメソッドでアクセスすると、ツイートが投稿されます。 実際に投稿してみましょう。

ではコミットしましょう。

git add .
git commit -m "ツイートを投稿するためのフォームを作った"

完成

一通りの機能が実装できたので、完成です。ここまで3回にわたる長いチュートリアルについてきてくださり、ありがとうございました。

おまけ

プログラミングをするにあたって一番勉強しやすいのは「自分で何かを作る」ということです。 「作る」に関しても、別に1から作る必要はありません。すごく大変ですし、時間もかかります。 そこで、「既存のものを少し改造してみる」という勉強方法をおすすめします。

たとえば○○をいじったらどこが変わるのか、とか、○○を追加したらどうなるのか、とか、そういうことを考えながら、既存のものを少し改造してみると、プログラミングの勉強になります。

改造のヒントを少し書いておきますね

Level1

  • CSSを書いてより見栄えを良くする
  • ツイート一覧にいま本文が表示されているだけなので、ユーザー名や投稿日時も表示するようにする
  • ツイートの詳細画面を作る

Level2

  • ユーザーのプロフィールシステム(Bioとか)を実装する
  • ツイートを削除・編集できるようにする

Level3

  • いいね機能を実装する
  • リプライ機能を実装する
  • このサイトをデプロイ(世界に公開)する

LevelMax

  • ユーザー認証を実装する
  • フォロー機能を実装する
  • セキュリティホールを探す(実はこのチュートリアルにはセキュリティホールがあります)