初めに
以下記事は、環境変数を用いたデータベース接続(todoアプリケーションでの)についての記事ですが全体的なコードをこちらの記事で投稿しようと思います。
構成
今回のアプリケーションは、以下の構成で実装します。
- GoでREST APIのサーバーを実装
- MySQLでデータの永続化を行う
- ReactとTypeScriptでフロントエンドを実装
▼ ディレクトリ構造
.
├── client
│ ├── src
│ │ ├── App.tsx
│ ├── index.tsx
│ │ ├── components
│ │ │ └── TodoList.tsx
│ └── ...
├── server
│ ├── main.go
│ ├── handlers
│ │ ├── todo.go
│ ├── models
│ │ ├── todo.go
├── .env(隠しファイル)
│ └── ...
└── database
└── init.sql
サーバー&データベース側のコード
▼ server/main.go
package main
import (
"go-typescript-original-todo/handlers"
"net/http"
"github.com/rs/cors"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/todos", handlers.TodoHandler)
handler := cors.New(cors.Options{
AllowedOrigins: []string{"http://localhost:3000"},
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowedHeaders: []string{"*"},
}).Handler(mux)
http.ListenAndServe(":8080", handler)
}
▼ server/handlers/todo.go
package handlers
import (
"encoding/json"
"go-typescript-original-todo/models"
"net/http"
"strconv"
)
var todoModel = models.NewTodoModel()
func TodoHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getTodos(w, r)
case http.MethodPost:
createTodo(w, r)
case http.MethodPut:
updateTodo(w, r)
case http.MethodDelete:
deleteTodo(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func getTodos(w http.ResponseWriter, r *http.Request) {
// Get all todos from the model and return as JSON
todos, err := todoModel.GetTodos()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(todos)
}
func createTodo(w http.ResponseWriter, r *http.Request) {
// Create a new todo with the data from the request
var todo models.Todo
// Parse the JSON request body and decode it into the Todo struct
err := json.NewDecoder(r.Body).Decode(&todo)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Use the model to create a new todo in the database
err = todoModel.CreateTodo(&todo)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set the response header and send the created todo as JSON
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(todo)
}
func updateTodo(w http.ResponseWriter, r *http.Request) {
// Parse the JSON request body and decode it into the Todo struct
var todo models.Todo
err := json.NewDecoder(r.Body).Decode(&todo)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Get the ID from the query parameters
idStr := r.URL.Query().Get("id")
if idStr == "" {
http.Error(w, "Missing 'id' parameter", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid 'id' parameter", http.StatusBadRequest)
return
}
// Get the existing Todo
existingTodo, err := todoModel.GetTodo(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Update the fields of the existing Todo if they were provided in the request
if todo.Title != "" {
existingTodo.Title = todo.Title
}
if todo.Description != "" {
existingTodo.Description = todo.Description
}
if todo.Completed != existingTodo.Completed {
existingTodo.Completed = todo.Completed
}
// Use the model to update the todo in the database
err = todoModel.UpdateTodo(id, existingTodo)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set the response header and send the updated todo as JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(existingTodo)
}
func deleteTodo(w http.ResponseWriter, r *http.Request) {
// Delete an existing todo with the given ID
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = todoModel.DeleteTodo(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
▼ server/models/todo.go
package models
import (
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv"
)
type TodoModel struct {
db *sql.DB
}
func NewTodoModel() *TodoModel {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
user := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
host := os.Getenv("DB_HOST")
port := os.Getenv("DB_PORT")
dbName := os.Getenv("DB_NAME")
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, password, host, port, dbName)
db, err := sql.Open("mysql", connectionString)
if err != nil {
panic(err)
}
return &TodoModel{db: db}
}
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (t *TodoModel) GetTodos() ([]Todo, error) {
// Get all todos from the database
query := "SELECT id, title, description, completed, created_at, updated_at FROM todos"
rows, err := t.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var todos []Todo
for rows.Next() {
var todo Todo
err := rows.Scan(&todo.ID, &todo.Title, &todo.Description, &todo.Completed, &todo.CreatedAt, &todo.UpdatedAt)
if err != nil {
return nil, err
}
todos = append(todos, todo)
}
if err = rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func (t *TodoModel) CreateTodo(todo *Todo) error {
// Insert a new todo into the database
currentTime := time.Now().Format("2006-01-02 15:04:05")
todo.CreatedAt = currentTime
todo.UpdatedAt = currentTime
query := "INSERT INTO todos (title, description, completed, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
result, err := t.db.Exec(query, todo.Title, todo.Description, todo.Completed, todo.CreatedAt, todo.UpdatedAt)
if err != nil {
return err
}
// Retrieve the ID of the newly inserted todo
id, err := result.LastInsertId()
if err != nil {
return err
}
todo.ID = int(id)
return nil
}
func (t *TodoModel) UpdateTodo(id int, todo *Todo) error {
// Update an existing todo in the database
currentTime := time.Now().Format("2006-01-02 15:04:05")
todo.UpdatedAt = currentTime
query := "UPDATE todos SET title = ?, description = ?, completed = ?, updated_at = ? WHERE id = ?"
_, err := t.db.Exec(query, todo.Title, todo.Description, todo.Completed, todo.UpdatedAt, id)
if err != nil {
return err
}
return nil
}
func (t *TodoModel) DeleteTodo(id int) error {
// Delete an existing todo from the database
query := "DELETE FROM todos WHERE id = ?"
_, err := t.db.Exec(query, id)
if err != nil {
return err
}
return nil
}
func (t *TodoModel) GetTodo(id int) (*Todo, error) {
// Get a todo from the database by id
query := "SELECT id, title, description, completed, created_at, updated_at FROM todos WHERE id = ?"
row := t.db.QueryRow(query, id)
var todo Todo
err := row.Scan(&todo.ID, &todo.Title, &todo.Description, &todo.Completed, &todo.CreatedAt, &todo.UpdatedAt)
if err != nil {
return nil, err
}
return &todo, nil
}
▼ database/init.sql
CREATE DATABASE IF NOT EXISTS todo_app;
USE todo_app;
CREATE TABLE IF NOT EXISTS todos (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
completed BOOLEAN NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
フロント側のコード
▼ client/src/App.tsx
import TodoList from "./components/TodoList";
function App() {
return (
<>
<TodoList />
</>
)
}
export default App;
▼ client/src/components/TodoList.tsx
import { useEffect, useState } from "react";
import axios from "axios";
import './TodoList.css';
interface Todo {
id: number;
title: string;
description: string;
completed: boolean;
created_at: string;
updated_at: string;
}
enum TodoView {
All,
Completed,
Uncompleted,
}
const TodoList = () => {
const [todos, setTodos] = useState<Todo[] | null>([]);
const [todoView, setTodoView] = useState<TodoView>(TodoView.All);
const [isMaxTodos, setIsMaxTodos] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [updatingTodoId, setUpdatingTodoId] = useState<number | null>(null);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = () => {
axios.get('http://localhost:8080/todos')
.then((response) => {
setTodos(response.data);
setIsMaxTodos(response.data >= 10);
})
.catch((error) => {
console.log('Error fetching todos:', error);
setTodos(null);
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isMaxTodos) {
alert('Cannot add more than 10 todos');
return;
}
if (updatingTodoId) {
axios.put(`http://localhost:8080/todos?id=${updatingTodoId}`, {
title,
description,
completed: false,
})
.then(() => {
setTitle("");
setDescription("");
setUpdatingTodoId(null);
fetchTodos();
})
.catch((error) => {
console.error('Error updating todo:', error);
});
} else {
axios.post('http://localhost:8080/todos', {
title,
description,
completed: false,
})
.then(() => {
setTitle("");
setDescription("");
fetchTodos();
})
.catch((error) => {
console.error('Error creating todo:', error);
});
}
};
const handleDelete = (id: number) => {
axios.delete(`http://localhost:8080/todos?id=${id}`)
.then(() => {
fetchTodos();
})
.catch((error) => {
console.error("Error deleting todo:", error);
alert(`Error deleting todo: ${error.message || 'Unknown error'}`);
});
};
const handleUpdate = (todo: Todo) => {
setUpdatingTodoId(todo.id);
setTitle(todo.title)
setDescription(todo.description);
};
const handleToggleCompleted = (id: number, completed: boolean) => {
axios.put(`http://localhost:8080/todos?id=${id}`, {
completed: !completed,
})
.then(() => {
fetchTodos();
})
.catch((error) => {
console.error('Error updating todo:', error);
})
};
const showAllTodos = () => {
setTodoView(TodoView.All);
};
const showCompletedTodos = () => {
setTodoView(TodoView.Completed);
};
const showUncompletedTodos = () => {
setTodoView(TodoView.Uncompleted);
};
let displayedTodos;
if (todos) {
switch (todoView) {
case TodoView.All:
displayedTodos = todos;
break;
case TodoView.Completed:
displayedTodos = todos.filter((todo) => todo.completed);
break;
case TodoView.Uncompleted:
displayedTodos = todos.filter((todo) => !todo.completed);
break;
default:
displayedTodos = todos;
}
};
return (
<div className="todo-list">
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<label>
Title:
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</label>
<label>
Description:
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</label>
<button type="submit">{updatingTodoId ? 'Update Todo': 'Add Todo'}</button>
</form>
<button onClick={showAllTodos}>Show All Todos</button>
<button onClick={showCompletedTodos}>Show Completed Todos</button>
<button onClick={showUncompletedTodos}>Show Uncompleted Todos</button>
<ul>
{Array.isArray(displayedTodos) && displayedTodos.map((todo) => (
<li key={todo.id}>
<h2>{todo.title}</h2>
<p>{todo.description}</p>
<p>Completed: {todo.completed ? 'Yes' : 'No'}</p>
<p>Created at: {todo.created_at}</p>
<p>Updated at: {todo.updated_at}</p>
<button className="update-button" onClick={() => handleUpdate(todo)}>Update</button>
<button className="delete-button" onClick={() => handleDelete(todo.id)}>Delete</button>
<label>
Completed:
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleCompleted(todo.id, todo.completed)}
/>
</label>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
最後に
コードで誤っている点がありましたらご指摘のほどお待ちしております!