Actix-web + Tera + SQLite の組み合わせで CRUD を実装しました。Html も複数使って画面を切り替えています。 CSS も使って画面を整えています。
2025年9月25日
index.html と edit.html を一部変更しました。
2025年9月29日 この節を追加しました。
このコーナーでは、データベースとして SQLite を使っています。 SQLite を次のようにしてインストールしてください。
# Debian 系にインストールする
sudo apt install sqlite3
sudo apt install libsqlite3-dev
# RPM 系にインストールする
sudo dnf install sqlite
sudo dnf install sqlite-devel
# macOS にインストールする、次のコマンドだけでデベロッパーライブラリもインストールされます。
brew install sqlite
# Windows にインストールする
SQLite をインストールすることはできますが、デベロッパーライブラリのインストールにはまだ成功していません。
ライブラリのインストールは不要との説もありますが、実際には動作しません。
[package]
name = "tera"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-files = "0.6.8"
actix-web = "4.11.0"
rusqlite = "0.37.0"
serde = { version = "1.0.226", features = ["derive"] }
tera = "1.20.0"
//--------------------------------
// main.rs
// copyright : vivacocoa.jp
// last modified: Sep. 24, 2025
//--------------------------------
use actix_web::{web, App, Responder, HttpResponse, HttpServer};
use actix_web::web::Redirect;
use tera::{Tera, Context};
use rusqlite::{Connection, params};
use serde::{Serialize, Deserialize};
use actix_files as fs;
// Todoのデータ構造を定義
#[derive(Serialize, Deserialize)]
struct Todo {
id: i32,
title: String,
completed: bool,
}
#[actix_web::main]
async fn main() -> std::io::Result<()>
{
let conn = Connection::open("todo.db"); // データベースファイルを開く/作成する
// テーブルが存在しない場合に作成する
let _ = conn.expect("REASON").execute(
"CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN NOT NULL
)",
[],
);
HttpServer::new(|| {
// templatesディレクトリ内のHTMLを読み込む
let tera = Tera::new("templates/**/*").expect("Failed to initialize Tera");
App::new()
.app_data(web::Data::new(tera))
.service(fs::Files::new("/static", "./static").show_files_listing())
.service(web::resource("/").to(index))
.service(web::resource("/create").to(create))
.service(web::resource("/update").to(update))
.service(web::resource("/delete").to(delete))
.service(web::resource("/edit").to(edit))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn index(tera: web::Data<Tera>) -> impl Responder
{
let conn = Connection::open("todo.db").unwrap();
let mut stat = conn.prepare("SELECT * FROM todos").unwrap();
let mut rows = stat.query([]).unwrap();
let mut todos = Vec::new();
while let Ok(Some(row)) = rows.next() {
let i_d: i32 = row.get(0).unwrap();
let todo: String = row.get(1).unwrap();
let comp: bool = row.get(2).unwrap();
todos.push(Todo {id: i_d, title: todo, completed: comp});
}
let mut context = tera::Context::new();
context.insert("todos", &todos);
let rendered = tera.render("index.html", &context).unwrap(); // index.htmlをレンダリング
HttpResponse::Ok().body(rendered)
}
async fn create(form: web::Form<Todo>) -> Redirect
{
let conn = rusqlite::Connection::open("todo.db").unwrap();
conn. execute ( "INSERT INTO todos (title, completed) VALUES (?1, ?2)", params![form.title, form.completed],).unwrap();
Redirect::to("/")
}
async fn update(form: web::Form<Todo>) -> Redirect
{
let conn = rusqlite::Connection::open("todo.db").unwrap();
conn. execute (
"UPDATE todos SET completed = ?1, title = ?2 WHERE id = ?3",
params![form.completed, form.title, form.id],).unwrap();
Redirect::to("/")
}
async fn delete(form: web::Form<Todo>) -> Redirect
{
let conn = rusqlite::Connection::open("todo.db").unwrap();
conn. execute (
"DELETE FROM todos WHERE id = ?1",
params![form.id],).unwrap();
Redirect::to("/")
}
async fn edit(tera: web::Data<Tera>, form: web::Form<Todo>) -> impl Responder
{
let mut context = Context::new();
context.insert("i_d", &form.id);
context.insert("msg", &form.title);
context.insert("comp", &form.completed);
let rendered = tera.render("edit.html", &context).unwrap();
HttpResponse::Ok().body(rendered)
}
プロジェクトのルートディレクトリに static というディレクトリを作り、 その中に次の style.css を作ってください。
/*****************************
style.css
copyright : vivacocoa.jp
last modified: Sep. 24, 2025
******************************/
body {
background-color: #efefef;
}
.container {
width: 500px;
margin: 16px auto;
}
h1 {
font-size: 20px;
}
input {
height: 25px;
width: 100%;
padding: 4px;
border: solid 0px #efefef;
border-radius: 4px;
}
button {
background-color: white;
color: gray;
border: solid 1px lightgray;
border-radius: 4px;
margin-left: 4px;
}
.todo {
display: flex;
height: 80px;
width: 100%;
padding: 0px;
border-radius: 4px;
padding: 0px 0px;
line-height: 80px;
box-sizing: border-box;
background-color: white;
margin-bottom: 0px;
margin-top: 16px;
position: relative;
}
.checkbox {
margin: 33px 4px;
width: 14px;
height: 14px;
}
.btn-edit {
position: absolute;
width: 50px;
height: 30px;
right: 4px;
bottom: 4px;
}
.btn-delete {
position: absolute;
margin-top: 8px;
margin-left: 0px;
height: 35px;
left: 0px;
}
.btn-back {
position: absolute;
margin-top: 8px;
height: 35px;
right: 0px;
}
.h-layout {
display: flex;
}
.h-layout2 {
display: flex;
position: relative;
}
プロジェクトのルートディレクトリに templates というディレクトリを作り、 その中に次の index.html を作ります。
2025年9月25日
一部変更しました。
<!-----------------------------
index.html
copyright : vivacocoa.jp
last modified: Sep. 25, 2025
------------------------------>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Todo</h1>
<form method="post" action="/create">
<div class="h-layout">
<input type="text" name="title" required maxlength="29">
<input type="hidden" name="completed" value="false">
<input type="hidden" name="id" value="0">
<button>Create</button>
</div>
</form>
{% for todo in todos %}
<div class="todo">
<form method="post" action="/update" id=f{{todo.id}}>
<input type="checkbox" {% if todo.completed %}checked{% endif %} onchange="cbchange('f{{todo.id}}')"
class="checkbox">
<input type="hidden" name="id" value={{todo.id}}>
<input type="hidden" name="title" value="{{todo.title}}">
{% if todo.completed %}
<input type="hidden" name="completed" value="false">
{% else %}
<input type="hidden" name="completed" value="true">
{% endif %}
<button hidden>submit</button>
</form>
{% if todo.completed %}
<del>{{ todo.title }}</del>
{% else %}
{{ todo.title }}
{% endif %}
<form method="post" action="/edit">
<input type="hidden" name="id" value={{todo.id}}>
<input type="hidden" name="title" value="{{todo.title}}">
<input type="hidden" name="completed" value={{todo.completed}}>
<button class="btn-edit">Edit</button>
</form>
</div>
{% else %}
<div class="todo">No todo</div>
{% endfor %}
<script>
function cbchange(f) {
const form = document.getElementById(f);
form.submit();
}
</script>
</div>
</body>
</html>
templates ディレクトリに次の edit.html を作ってください。
2025年9月25日
一部変更しました。
<!-----------------------------
edit.html
copyright : vivacocoa.jp
last modified: Sep. 25, 2025
------------------------------>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit | Todo</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Edit</h1>
<form method="post" action="/update">
<div class="h-layout">
<input type="text" name="title" required maxlength="29" value="{{msg}}">
<input type="hidden" name="completed" value={{comp}}>
<input type="hidden" name="id" value={{i_d}}>
<button>Update</button>
</div>
</form>
<div class="h-layout2">
<form method="post" action="/delete">
<input type="hidden" name="id" value={{i_d}}>
<input type="hidden" name="title" value="{{msg}}">
<input type="hidden" name="completed" value={{comp}}>
<button class="btn-delete" onclick="return confirm('削除してよろしいですか?');">Delete</button>
</form>
<form method="get" action="/">
<button class="btn-back">Back to index</button>
</form>
</div>
</div>
</body>
</html>
ターミナルでプロジェクトのルートディレクトリに移動して、cargo run とコマンドしてサーバーを起動してください。 そしてお使いのブラウザで localhost:8080 もしくは 127.0.0.1:8080 を開いてください。
本番サーバーで試したい場合は、 簡易デプロイ を参考にしてください。