目次
目次
リポジトリ
- kawa1214/inkclip-backend (public)
- kawa1214/inkclip-frontend (private)
- kawa1214/inkclip-extension (private)
サービス概要
ブックマークからノートを作成出来るサービスです。(現在はクローズ済みです)
ブックマーク機能が上手く使いこなせていないこと、少ない労力で情報の整理が出来ないかと考え作成しました。
機能
アカウント
- 会員登録
- メール認証
- アカウント削除
- ログイン
- ChromeExtension からのログイン
ブックマーク
- ブックマーク追加・削除
- ChromeExtension からのブックマーク追加
ノート
- ノート作成・削除
- ノートへのブックマーク追加
- ブックマークからテーブル・リスト作成
- ブックマークしたページのプレビュー
バックエンド
- Go
- PostgresSQL
- sqlc
- gin
- gomock
Go の採用
リソースが少ないため言語仕様の変更が少ないこと。他言語の良いやり方を知るために Go を採用しました。
Handler の処理について
サービスリリースを優先しビジネスロジックは Handler に直接記述しています。
サービスとしてコアな機能の特定をした後に Clean Architecture などの構成を目指しましたが、その前にクローズしてしまいました。
sqlc
標準ライブラリの database/sql,gorm,sqlx,sqlc を検討しました。
- database/sql: 高速だが手動でマッピングが必要
- gorm: 手動でマッピングする必要はないがパフォーマンスが低い
- sqlx: 標準ライブラリとほぼ同じ速度かつ、フィールドのマッピングを自動で行う
- sqlc: SQL を書くと タイプセーフ コードが生成される
実装が早く行えることとタイプセーフなコードを書くために、sqlc を採用しました。
add_users.up.sql
, user.sql
コードを書きコマンドを実行すると、user.sql.go
が生成されます。
user.sql.go
に対しテストを書くことで、クエリ単位で安全なコードを書くことができます。
add_users.up.sql
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT (gen_random_uuid()),
"email" varchar NOT NULL,
"hashed_password" varchar NOT NULL,
"password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01 00:00:00Z',
"created_at" timestamptz NOT NULL DEFAULT (now())
);
user.sql
-- name: CreateUser :one
INSERT INTO users (
email,
hashed_password
) VALUES (
$1, $2
)
RETURNING *;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;
user.sql.go
const createUser = `-- name: CreateUser :one
INSERT INTO users (
email,
hashed_password
) VALUES (
$1, $2
)
RETURNING id, email, hashed_password, password_changed_at, created_at
`
type CreateUserParams struct {
Email string `json:"email"`
HashedPassword string `json:"hashed_password"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.HashedPassword)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.HashedPassword,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
const getUserByEmail = `-- name: GetUserByEmail :one
SELECT id, email, hashed_password, password_changed_at, created_at FROM users
WHERE email = $1 LIMIT 1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.HashedPassword,
&i.PasswordChangedAt,
&i.CreatedAt,
)
return i, err
}
OpenAPI
Open API 定義には、swaggo/swag
を利用しました。
デメリットとして、構造体を指定することで実装と仕様は一致しやすいものの、コードと実態の乖離がおこりえます。
openAPI generator のような、仕様からコードを生成するアプローチと比較すると手軽に導入できるというメリットがあります。
type renewAccessTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type renewAccessTokenResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
}
// @Param request body api.renewAccessTokenRequest true "query params"
// @Success 200 {object} api.renewAccessTokenResponse
// @Router /users/renew_access [post]
// @Tags user
func (server *Server) renewAccessToken(ctx *gin.Context) {
// ...
}
インフラ
- Docker
- fly.io
プロジェクト間の切り替えの速さやメンテナンスのしやすさから、開発環境から本番環境まですべてコンテナで開発しています。
fly.io は初めて使いましたが、料金が比較的安く、わかりやすいため個人開発ではファーストチョイスになりそうです。
フロントエンド
- Next.js
- TypeScript
- MUI
- swr
API Scheema
OpenAPI から TypeScript の API スキーマを生成し、それを swr で利用する形にしています。
OpenAPI の定義と乖離した実装にならないことが、型定義から保証されます。
【OpenAPI】API スキーマから勝手に型がつく axios を作って幸せになる【openapi-typescript】 がとても参考になりました。
Chrome Extension
- TypeScript
公式ドキュメントが充実しているため、こちらがとても参考になります。
注意点としては、v2 と v3 で仕様が違うため v3 側の仕様把握が必要です。