⚡️

bolt.new でフルスタックtodoアプリ

2024/12/21に公開

はじめに

https://bolt.new を使って、TODO アプリを作りました。

プロンプト

以下のようなプロンプトで生成しました。

バックエンドにsqliteを使ったAPIを提供し、フロントエンドでそのAPIにアクセスして表示するtodo アプリ

sqlite 使ってくれなかったので、さらに伝えてみました。

server/index.jsを、sqliteを使ってtodoデータを保存できるようにして

bolt.new の画面

こんな感じでチャット画面とプレビュー画面が見れます。

コードも見れます。
今回、バックエンドとフロントエンドを別で動くようにして Web API で通信できるようにしました。

バックエンドコードの実行は bolt がやってくれてました。

フロントエンドの実行は、bolt はしてくれなかったようなので、自分でターミナルを追加して実行しました。

バックエンドとフロントエンドを分離したので、ポートを指定することでどちらのサービスを表示するか切り替えられるようになってました。

コード

以下、主要なコードの抜粋です。
指定しないと、バックエンドは js で express になりました。
構造化はされてませんでした。
エラーハンドリングはしてました。

sqlite アクセス

直接テーブル作成 SQL が書かれてました。

import sqlite3 from "sqlite3";
import { open } from "sqlite";

// Initialize database connection
export async function initializeDB() {
  const db = await open({
    filename: "todos.db",
    driver: sqlite3.Database,
  });

  // Create todos table if it doesn't exist
  await db.exec(`
    CREATE TABLE IF NOT EXISTS todos (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      completed BOOLEAN NOT NULL DEFAULT 0,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  return db;
}

API

update が改善の余地アリかもです。

import express from "express";
import cors from "cors";
import { initializeDB } from "./db.js";

const app = express();
app.use(cors());
app.use(express.json());

let db;

// Initialize database connection
initializeDB()
  .then((database) => {
    db = database;
    console.log("Database connected successfully");
  })
  .catch((err) => {
    console.error("Failed to initialize database:", err);
    process.exit(1);
  });

// Get all todos
app.get("/api/todos", async (req, res) => {
  try {
    const todos = await db.all("SELECT * FROM todos ORDER BY created_at DESC");
    res.json(todos);
  } catch (err) {
    console.error("Error fetching todos:", err);
    res.status(500).json({ error: "Failed to fetch todos" });
  }
});

// Create todo
app.post("/api/todos", async (req, res) => {
  const { title } = req.body;
  try {
    const result = await db.run(
      "INSERT INTO todos (title, completed) VALUES (?, ?)",
      [title, false]
    );
    const todo = await db.get(
      "SELECT * FROM todos WHERE id = ?",
      result.lastID
    );
    res.json(todo);
  } catch (err) {
    console.error("Error creating todo:", err);
    res.status(500).json({ error: "Failed to create todo" });
  }
});

// Toggle todo completion
app.patch("/api/todos/:id", async (req, res) => {
  const { id } = req.params;
  try {
    const todo = await db.get("SELECT completed FROM todos WHERE id = ?", id);
    if (!todo) {
      return res.status(404).json({ error: "Todo not found" });
    }

    await db.run("UPDATE todos SET completed = ? WHERE id = ?", [
      !todo.completed,
      id,
    ]);

    const updatedTodo = await db.get("SELECT * FROM todos WHERE id = ?", id);
    res.json(updatedTodo);
  } catch (err) {
    console.error("Error updating todo:", err);
    res.status(500).json({ error: "Failed to update todo" });
  }
});

// Delete todo
app.delete("/api/todos/:id", async (req, res) => {
  const { id } = req.params;
  try {
    const result = await db.run("DELETE FROM todos WHERE id = ?", id);
    if (result.changes === 0) {
      return res.status(404).json({ error: "Todo not found" });
    }
    res.json({ success: true });
  } catch (err) {
    console.error("Error deleting todo:", err);
    res.status(500).json({ error: "Failed to delete todo" });
  }
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

API call

エラー等も考慮されてます。

import React, { useState, useEffect } from "react";
import { TodoItem } from "./components/TodoItem";
import { TodoForm } from "./components/TodoForm";
import { Loader2 } from "lucide-react";

interface Todo {
  id: number;
  title: string;
  completed: boolean;
  created_at: string;
}

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    try {
      setLoading(true);
      const response = await fetch("http://localhost:3000/api/todos");
      if (!response.ok) throw new Error("Failed to fetch todos");
      const data = await response.json();
      setTodos(data);
      setError(null);
    } catch (err) {
      setError("Failed to load todos. Please ensure the server is running.");
    } finally {
      setLoading(false);
    }
  };

  const addTodo = async (title: string) => {
    try {
      const response = await fetch("http://localhost:3000/api/todos", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title }),
      });
      if (!response.ok) throw new Error("Failed to add todo");
      const todo = await response.json();
      setTodos([todo, ...todos]);
      setError(null);
    } catch (err) {
      setError("Failed to add todo. Please try again.");
    }
  };

  const toggleTodo = async (id: number) => {
    try {
      const response = await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: "PATCH",
      });
      if (!response.ok) throw new Error("Failed to update todo");
      const updatedTodo = await response.json();
      setTodos(todos.map((todo) => (todo.id === id ? updatedTodo : todo)));
      setError(null);
    } catch (err) {
      setError("Failed to update todo. Please try again.");
    }
  };

  const deleteTodo = async (id: number) => {
    try {
      const response = await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: "DELETE",
      });
      if (!response.ok) throw new Error("Failed to delete todo");
      setTodos(todos.filter((todo) => todo.id !== id));
      setError(null);
    } catch (err) {
      setError("Failed to delete todo. Please try again.");
    }
  };

  if (loading) {
    return (
      <div className="min-h-screen bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center">
        <div className="bg-white p-8 rounded-lg shadow-xl">
          <Loader2 className="w-8 h-8 animate-spin text-indigo-600" />
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-indigo-500 to-purple-600 py-12 px-4">
      <div className="max-w-3xl mx-auto">
        <div className="bg-white rounded-lg shadow-xl overflow-hidden">
          <div className="p-8">
            <h1 className="text-4xl font-bold text-gray-800 mb-8">Todo App</h1>

            {error && (
              <div className="mb-4 p-4 bg-red-50 border border-red-100 text-red-600 rounded-lg flex items-center gap-2">
                <span className="flex-1">{error}</span>
                <button
                  onClick={fetchTodos}
                  className="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 rounded-md transition-colors duration-200"
                >
                  Retry
                </button>
              </div>
            )}

            <TodoForm onSubmit={addTodo} />

            <div className="space-y-4">
              {todos.map((todo) => (
                <TodoItem
                  key={todo.id}
                  {...todo}
                  onToggle={toggleTodo}
                  onDelete={deleteTodo}
                />
              ))}
              {todos.length === 0 && !error && (
                <p className="text-center text-gray-500 py-4">
                  No todos yet. Add one above!
                </p>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;

デプロイ(できなかった)

デプロイしようとしたら下記のように言われて無理でした。バックエンドはデプロイできないみたい。bolt.new はフルスタック!みたいな記事が多いけど、このコードを Netlify へのデプロイするのはできないみたいです。ブラウザ上でやってる分には、WebContainer API 上でコンテナ立てて、node が動いてるから、バックエンドも開発できるけど、Netlify にデプロイしようと思うと、ブラウザに配信される html, javascript, css になって node は動かせないって感じかと思います。

もしかしたら、そのうち行けるようになるんかな。スピード早すぎるから記事書いてる間にもういけてる可能性も。。。

以下、チャットが返してくれたメッセージ。

I apologize, but I cannot deploy this application as is. This is a full-stack application with a Node.js backend and SQLite database, which cannot be deployed to static hosting providers like Netlify.

For deployment, you have a few options:

Split the application:

Deploy only the frontend to Netlify
Deploy the backend separately to a platform that supports Node.js (like Heroku, Railway, or Render)
Convert to a static application:

Remove the backend
Use local storage or a cloud database service instead of SQLite
Would you like me to help you with either of these approaches? I can:

Modify the app to be frontend-only using local storage for persistence
Keep the current architecture and deploy only the frontend, and I can guide you on deploying the backend separately
Please let me know which approach you'd prefer to take.

おわりに

そのうち、ちゃんと Next.js で試してみたい。試そうとしたら、トークン制限が来た。。。

GitHubで編集を提案

Discussion