目次
目次
リポジトリ
- 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 側の仕様把握が必要です。