Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
WebAPIのバリデーションを、型の力でいい感じにする
Search
takuya kikuchi
September 29, 2023
0
68
WebAPIのバリデーションを、型の力でいい感じにする
Web API LT会 - vol.3 のLT資料です #webapilt
takuya kikuchi
September 29, 2023
Tweet
Share
More Decks by takuya kikuchi
See All by takuya kikuchi
生成AI時代のソフトウェアエンジニアが持つべきケイパビリティを考える
tkikuchi1002
8
4.8k
RAGをテーマに考える、LLMの認知アーキテクチャとソフトウェア設計
tkikuchi1002
3
1.1k
生成AIの不確実性と向き合うためのオブジェクト指向設計
tkikuchi1002
3
6.5k
Azure AI SearchとPromptFlowではじめるRAG
tkikuchi1002
2
1.2k
法人向けChatGPTにおける Azure OpenAI Serviceの課題解決の過程と現在
tkikuchi1002
2
1.9k
LLMエンジニアリングを加速させるソフトウェアアーキテクチャ
tkikuchi1002
2
5.2k
GoとDDDでモバイルオーダープラットフォームを 型安全に作り直した話
tkikuchi1002
0
83
Kotlinのcoroutine、async/awaitと同じでしょ?って思ってたけど意外と洗練されててすごいなぁって思った話をさせてほしい
tkikuchi1002
0
89
使いやすいインターフェースについて考える
tkikuchi1002
0
28
Featured
See All Featured
Learning to Love Humans: Emotional Interface Design
aarron
273
40k
The Cult of Friendly URLs
andyhume
78
6k
The Power of CSS Pseudo Elements
geoffreycrofte
73
5.3k
Build The Right Thing And Hit Your Dates
maggiecrowley
33
2.4k
What’s in a name? Adding method to the madness
productmarketing
PRO
22
3.1k
Principles of Awesome APIs and How to Build Them.
keavy
126
17k
StorybookのUI Testing Handbookを読んだ
zakiyama
27
5.3k
Happy Clients
brianwarren
98
6.7k
Large-scale JavaScript Application Architecture
addyosmani
510
110k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
27
840
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
25
1.8k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
27
4.3k
Transcript
confidential WebAPIのバリデーションを、型の力でいい感じにする Web API LT会 - vol.3 takuya kikuchi /
Showcase Gig
confidential ©Showcase Gig 自己紹介 • takuya kikuchi • twitter: @_pochi
• Engineer Group Manager @ Showcase Gig • モバイルオーダープラットフォームを作っています • たまに実店舗も作ります
confidential ©Showcase Gig APIが受け取るパラメータのバリデーション ちゃんとできてますか? SDK, OpenAPI, gRPC, etc… 「そんな入力予期してなかった・・・」
「そんな使い方しないで・・・」
confidential ©Showcase Gig 素直なアプローチ紹介 gRPCの例 protoc-gen-validate を使ってバリデータの自動生成ができるよ 🐔 参考: 【Go】gRPCのリクエストバリデータを自動生成する
https://note.com/scg_tech/n/nb12a33bfd391 import "github.com/envoyproxy/protoc-gen-validate/validate/validate.proto"; ~~~~~~~~~~~ service TestServer { rpc Test(TestMessage) returns (Result) {} } ~~~~~~~~~~~ message TestMessage { // 0~100間の整数 int32 seisuu = 1 [(validate.rules).int32 = {gte:0, lt: 100}]; // floatで0~1の値 double fudou = 2 [(validate.rules).double = {gte: 0, lte: 1}]; // アルファベットと数値で、5〜30文字のrepeated repeated string mojiretsu = 3 [(validate.rules).repeated.items.string = {pattern: "^[a-z0-9]{5,30}$", min_len: 5, max_len:30}]; // RFC 1034で解釈可能なメールアドレス string mail_address = 4 [(validate.rules).string.email = true]; }
confidential ©Showcase Gig 型の話をします
confidential ©Showcase Gig 型はいいぞ • 静的型付け言語では、コンパイル時に型チェックをしてくれる • 型の明らかな渡し間違いはすぐ気づくことができる import "fmt"
func main() { var intValue int = 12345 printString (intValue) } func printString (str string) { fmt.Printf("%s\n", str) } import "fmt" func main() { var str string = "文字列だよ" printString (str) } func printString (str string) { fmt.Printf("%s\n", str) } コンパイルOK コンパイルエラー
confidential ©Showcase Gig しかし基本データ型には限界がある • メールアドレスのみを受け入れたいのだけど、コンパイラさん気づいてくれない import "fmt" func main()
{ var mailAddress string = "メアドではないよ" printMailAddress (mailAddress) } func printMailAddress (mailAddress string) { fmt.Printf("%s\n", mailAddress) } import "fmt" func main() { var mailAddress string = "
[email protected]
" printMailAddress (mailAddress) } func printMailAddress (mailAddress string) { fmt.Printf("%s\n", mailAddress) } コンパイルOK 🤗 コンパイルOK 😢
confidential ©Showcase Gig しかし基本データ型には限界がある • そもそもユーザーから任意の値が入力されるものはどうしようもない import "fmt" func main()
{ var str string // ユーザー入力を受け取る _, err := fmt.Scan(&str) if err != nil { // 読み取り失敗 return } printMailAddress(str) } コンパイルOK。ただしメアド以外も printできてしまう。
confidential ©Showcase Gig さてどうする
confidential ©Showcase Gig 「メールアドレス」型を作ろう • 正しいメアド入力以外はオブジェクトを生成できないようにする type MailAddress string func
NewMailAddress (mailAddress string) (MailAddress , error) { if !isValidMailAddress (mailAddress) { return "", errors.New("メアドではないので NG") } return MailAddress (mailAddress) , nil } func printMailAddress(mailAddress MailAddress) { fmt.Printf("%s\n", mailAddress) } 型とコンストラクタを定義。コンストラクタで、「正しいメールアド レスかどうか」をチェックする printMailAddressメソッドの引数も、その型を受け取るようにする
confidential ©Showcase Gig 「メールアドレス」型を活用する • コンパイル時チェックはできないが、「メールアドレスじゃない文字列が渡された場合」のエラーハンドリング実装が強制さ れる ◦ なので、「うっかりメアドじゃない文字列が渡されてクラッシュ!」みたいなことが回避できる func
main() { var str string // ユーザー入力を受け取る _, err := fmt.Scan(&str) if err != nil { // 読み取り失敗 return } // 入力された文字列から、 MailAddressオブジェクト生成 mailAddress, err := NewMailAddress (str) if err != nil { // メールアドレスではない文字列が渡された return } // MailAddress を出力する printMailAddress (mailAddress) }
confidential ©Showcase Gig 本題。WebAPIの話
confidential ©Showcase Gig メールアドレスを登録するWebAPI • メールアドレスを受け取ってDBに記録するエンドポイントのコントローラをイメージしてください • MailAddressRepositoryは単にSQLを呼ぶだけの実装だと思ってください // メールアドレスを登録するエンドポイントの実装
func updateMailAddressController (userID int, mailAddress string) error { // DBアクセス用のリポジトリ取得 repository := newMailAddressRepository () // DBに書き込み err := repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil } type MailAddressRepository interface { Update(userID int, mailAddress string) error }
confidential ©Showcase Gig 問題点 • mailAddress のバリデーションをしていない。 ◦ 渡されてくるmailAddress次第でいろんなエラーが起きてしまう ▪
メアドじゃなくても登録できる(それはダメ!) ▪ 文字列が長すぎたらDBへのクエリでエラーが起きて internal error (カッコ悪い!) • メアドじゃない文字列が渡されたらクライアントエラーにしてあげたい // メールアドレスを登録するエンドポイントの実装 func updateMailAddressController (userID int, mailAddress string) error { // DBアクセス用のリポジトリ取得 repository := newMailAddressRepository () // DBに書き込み err := repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil }
confidential ©Showcase Gig 先程作ったメールアドレス型を使います • MailAddressRepositoryの引数を、string から MailAddress型に変更 type MailAddressRepository
interface { Update(userID int, mailAddress MailAddress) error } type MailAddressRepository interface { Update(userID int, mailAddress string) error }
confidential ©Showcase Gig するとどうなるか • 元のコードに戻ります。 • mailAddressが string なのでコンパイルエラーになります
// メールアドレスを登録するエンドポイントの実装 func updateMailAddressController (userID int, mailAddress string) error { // DBアクセス用のリポジトリ repository := newMailAddressRepository () // DBに書き込み err := repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil }
confidential ©Showcase Gig あとはコンパイルエラーを解消する • コンパイルエラーを解消するために、引数で渡された文字列をMailAddress型に変換します • その際にエラーハンドリングがごく自然に実装されます // メールアドレスを登録するエンドポイントの実装
func updateMailAddressController (userID int, mailAddressStr string) error { // DBアクセス用のリポジトリ repository := newMailAddressRepository () mailAddress, err := NewMailAddress(mailAddressStr) if err != nil { // 渡された文字列がメールアドレスではない return errors.New("client error") } // DBに書き込み err = repository. Update(userID, mailAddress) if err != nil { // DB書き込みエラー return errors.New("internal error" ) } // 正常終了 return nil }
confidential ©Showcase Gig まとめ リクエストパラメータをバリデーションしたいあなたへ → バリデーションしたいということは、その値には何らかのルールがある → そのルールを表現する、専用の型を用意してみよう →
専用の型の利用を強制しよう → すると、パラメータのバリデーションは自然に実装される
confidential ©Showcase Gig おしまい
confidential ©Showcase Gig 宣伝 • APIが好きな人 • 型が好きな人 • Goが好きな人
• gRPCが好きな人 • エンジニアリングが好きな人 • POSが好きな人 • 次世代店舗を作りたい人 https://www.showcase-gig.com/recruit/engineer/