こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
そして今回は、これまで実装してきたGolangとReactアプリを連携させたいと思います。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
<目次>
前回
事前準備
フォルダ/ファイル
1 |
touch backend-app/cmd/api/middleware.go |
モジュール
1 2 |
cd go-movies/ npm i axios |
プロジェクト
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
$ tree -I node* . ├── Dockerfile-golang ├── Dockerfile-postgres ├── Dockerfile-react ├── README.md ├── backend-app // golang │ ├── cmd │ │ └── api │ │ ├── main.go │ │ ├── middleware.go │ │ ├── movie-handlers.go │ │ ├── routes.go │ │ ├── statusHandler.go │ │ └── utilities.go │ ├── go.mod │ ├── go.sum │ ├── models │ │ ├── models.go │ │ └── movies-db.go │ └── tmp │ ├── air.log │ └── main ├── docker-compose.yml ├── go-movies // React │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Admin.tsx │ │ │ ├── Categories.tsx │ │ │ ├── Home.tsx │ │ │ ├── Movies.tsx │ │ │ └── OneMovie.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── models │ │ │ └── movie.ts │ │ ├── react-app-env.d.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ └── yarn.lock └── postgres └── go_movies.sql |
CORSの設定
GolangとReactはDocker環境で動いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# docker-compose.yml version: "3.3" services: backend: build: context: . dockerfile: ./Dockerfile-golang ports: - 4000:4000 volumes: - ./backend-app:/app frontend: build: context: . dockerfile: ./Dockerfile-react volumes: - ./go-movies:/go-movies command: > sh -c "npm run start" ports: - "3000:3000" container_name: frontend |
この時、サイトのオリジンがそれぞれ「http://localhost:3000」、「http://localhost:4000」と異なるため、アプリケーション間の通信ができません。
問題を回避するために、CORS(異なるオリジン間でリソースの受け渡しができる仕組み)の設定を追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
// cmd/api/middleware.go package main import "net/http" func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") next.ServeHTTP(w, r) }) } |
リクエストするHTTPヘッダーに「Access-Control-Allow-Origin, * 」を追加することで、全てのオリジンを許可しました。
これをroutesで使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// cmd/api/routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) // func (app *application) routes() *httprouter.Router { func (app *application) routes() http.Handler { ... return app.enableCORS(router) } |
Movieリストの取得
ReactからGolang APIへリクエストを送り、Movieリストを取得しましょう。
APIへのリクエストには、axiosを使います。
BaseURL
axiosのBaseURLを設定しておくと、APIのURLの記述が短く済みます。
1 2 3 4 5 6 7 8 |
// src/index.tsx ... import axios from 'axios' // 追加 axios.defaults.baseURL = 'http://localhost:4000/v1/' ReactDOM.render(...); |
Movies APIへリクエスト
MoviesコンポーネントからMovies APIへリクエストを送ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// components/Movies.tsx ... import axios from 'axios' const Movies = () => { // 仮のデータ // const tmpData = [ // {id: 1, title: "The Shawshank Redemption", runtime: 142}, // {id: 2, title: "Harry Potter", runtime: 175}, // {id: 3, title: "The Dark Kngiht", runtime: 142}, // ] const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) useEffect(() => { ( async () => { await axios.get('movies') // BaseURLを設定しているのでmoviesのみで良い .then((response) => { setMovies(response.data.movies) setIsLoaded(true) }) } )() }, []) if (!isLoaded) { return ( <p>Loading...</p> ) } return ( ... ) } export default Movies |
動作検証
「docker-compose up」でコンテナを立ち上げ、ブラウザから「http://localhost:3000」にアクセスしましょう。
それから、メニューバーの「Movies」タグをクリックします。
エラーハンドリング
誤ったURLを指定するなどして、APIへのリクエストがエラーになる場合があります。

こういった場合に備えて、エラーハンドリング(catch)を追加しておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
const Movies = () => { ... const [error, setError] = useState("") useEffect(() => { ( async () => { await axios.get('moviess') // moviessに変更しエラーを出す .then((response) => { setMovies(response.data.movies) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } ... } |

Movie APIへのリクエスト
Movieデータを取得する処理を実装します。
Movieモデルの更新
Movie APIからのレスポンスは、以下のようになっています。

これに合わせて、Movieモデルのインターフェースを更新します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// models/movie.ts export interface Movie { id: number title: string description: string year: number release_date: string runtime: number rating: number mpaa_rating: string genres: Array<string> } |
Axios
Movie APIへリクエストを送信する処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// components/OneMovie.tsx import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const OneMovie = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: 0, release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: {} }) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { ( async () => { await axios.get(`movie/${props.match.params.id}`) .then((response) => { setMovie(response.data.movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) // mapを使えるようにするために配列にする if (movie.genres) { movie.genres = Object.values(movie.genres) } else { movie.genres = [] } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } ... } |
ページへ反映
取得したMovieデータをページに反映させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// components/OneMovie.tsx ... const OneMovie = (props: any) => { .... return ( <Fragment> <h2>Movie: {movie.title} ({movie.year})</h2> <div className="float-start"> <small>Raging: {movie.mpaa_rating}</small> </div> <div className="float-end"> {movie.genres.map((m, index) => ( <span key={index} className="badge bg-secondary me-1"> {m} </span> ))} </div> <div className="clearfix"></div> <hr /> <table className="table table-compact table-striped"> <thead></thead> <tbody> <tr> <td><strong>Title:</strong></td> <td>{movie.title}</td> </tr> <tr> <td><strong>Description: </strong></td> <td>{movie.description}</td> </tr> <tr> <td><strong>Run Time:</strong></td> <td>{movie.runtime} minutes</td> </tr> </tbody> </table> </Fragment> ) } export default OneMovie |
検証
ブラウザから「http://localhost:3000/movies/1」へアクセスしてみましょう。

OKですね。
次回
次回は、Movieデータに付随してついてくるGeres(ジャンル)データを処理します。
記事まとめ
参考書籍
ソースコード
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// cmd/api/routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) func (app *application) routes() http.Handler { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/status", app.statusHandler) router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovie) return app.enableCORS(router) } |
middleware.go
1 2 3 4 5 6 7 8 9 10 11 12 |
// cmd/api/middleware.go package main import "net/http" func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") next.ServeHTTP(w, r) }) } |
index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import axios from 'axios' axios.defaults.baseURL = 'http://localhost:4000/v1/' ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); |
movie.ts
1 2 3 4 5 6 7 8 9 10 11 12 |
// models/movie.ts export interface Movie { id: number title: string description: string year: number release_date: string runtime: number rating: number mpaa_rating: string genres: Array<string> } |
Movies.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
// components/Movies.tsx import { Link } from 'react-router-dom' import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const Movies = () => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { ( async () => { await axios.get('movies') .then((response) => { setMovies(response.data.movies) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Choose a movie</h2> <ul> {movies.map((m) => { return ( <li key={m.id}> <Link to={`/movies/${m.id}`}>{m.title}</Link> </li> ) })} </ul> </Fragment> ) } export default Movies |
OneMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
// components/OneMovie.tsx import { useEffect, useState, Fragment } from 'react' import { Movie } from '../models/movie' import axios from 'axios' const OneMovie = (props: any) => { const [movie, setMovie] = useState<Movie>({ id: 0, title: '', description: '', year: 0, release_date: '', runtime: 0, rating: 0, mpaa_rating: '', genres: [] }) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { ( async () => { await axios.get(`movie/${props.match.params.id}`) .then((response) => { setMovie(response.data.movie) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) // mapを使えるようにするために配列にする if (movie.genres) { movie.genres = Object.values(movie.genres) } else { movie.genres = [] } if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Movie: {movie.title} ({movie.year})</h2> <div className="float-start"> <small>Raging: {movie.mpaa_rating}</small> </div> <div className="float-end"> {movie.genres.map((m, index) => ( <span key={index} className="badge bg-secondary me-1"> {m} </span> ))} </div> <div className="clearfix"></div> <hr /> <table className="table table-compact table-striped"> <thead></thead> <tbody> <tr> <td><strong>Title:</strong></td> <td>{movie.title}</td> </tr> <tr> <td><strong>Description: </strong></td> <td>{movie.description}</td> </tr> <tr> <td><strong>Run Time:</strong></td> <td>{movie.runtime} minutes</td> </tr> </tbody> </table> </Fragment> ) } export default OneMovie |
コメントを残す
コメントを投稿するにはログインしてください。