Skip to content

Inkclip

2022/10/01

目次

目次

  1. リポジトリ
  2. サービス概要
  3. 機能
    1. アカウント
    2. ブックマーク
    3. ノート
  4. バックエンド
    1. Go の採用
    2. Handler の処理について
    3. sqlc
    4. OpenAPI
  5. インフラ
  6. フロントエンド
    1. API Scheema
  7. Chrome Extension

リポジトリ

サービス概要

ブックマークからノートを作成出来るサービスです。(現在はクローズ済みです)

ブックマーク機能が上手く使いこなせていないこと、少ない労力で情報の整理が出来ないかと考え作成しました。

lp

bookmark

note

extension

機能

アカウント

ブックマーク

ノート

バックエンド

Go の採用

リソースが少ないため言語仕様の変更が少ないこと。他言語の良いやり方を知るために Go を採用しました。

Handler の処理について

サービスリリースを優先しビジネスロジックは Handler に直接記述しています。

サービスとしてコアな機能の特定をした後に Clean Architecture などの構成を目指しましたが、その前にクローズしてしまいました。

sqlc

標準ライブラリの database/sql,gorm,sqlx,sqlc を検討しました。

実装が早く行えることとタイプセーフなコードを書くために、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) {
  // ...
}

インフラ

プロジェクト間の切り替えの速さやメンテナンスのしやすさから、開発環境から本番環境まですべてコンテナで開発しています。

fly.io は初めて使いましたが、料金が比較的安く、わかりやすいため個人開発ではファーストチョイスになりそうです。

フロントエンド

API Scheema

OpenAPI から TypeScript の API スキーマを生成し、それを swr で利用する形にしています。

OpenAPI の定義と乖離した実装にならないことが、型定義から保証されます。

【OpenAPI】API スキーマから勝手に型がつく axios を作って幸せになる【openapi-typescript】 がとても参考になりました。

Chrome Extension

公式ドキュメントが充実しているため、こちらがとても参考になります。

注意点としては、v2 と v3 で仕様が違うため v3 側の仕様把握が必要です。