Help us understand the problem. What is going on with this article?

goaとxoでchat apiを作ってみた。

More than 1 year has passed since last update.

この記事はmacで動かす前提で書いています。
goのバージョンは1.8です。
jqとMakeがインストールされている前提です

こんな感じのurlでwebAPIを作りたいかなと漠然と考えました

methods エンドポイント 目的
get,post http://chat/api/rooms チャットルームの作成と一覧の取得
get,post http://chat/api/rooms/3/messages room3からのメッセージ取得と送信

またchatとして自動更新するようにwebsocketも使います。

エンドポイント 目的
ws://chat/api/rooms/3/watch room 3の変更通知受取

これを念頭にDSLを書きます

あとデータベースはmysqlを使います。xoでdbからコード生成を行う方針で行きます。

初期設定

goaのインストール

go get -u -v  github.com/goadesign/goa/..

GOPATH上にプロジェクトを作ります

自分の場合
mkdir -p $GOPATH/src/github.com/m0a-mystudy/goa-chat
cd $GOPATH/src/github.com/m0a-mystudy/goa-chat

goa-cellerを参考にMakefileを作ります。

#! /usr/bin/make
#
# Makefile for goa chat
#
# Targets:
# - clean     delete all generated files
# - generate  (re)generate all goagen-generated files.
# - build     compile executable
#
# Meta targets:
# - all is the default target, it runs all the targets in the order above.
#

all: clean generate build

clean:
    @rm -rf app
    @rm -rf client
    @rm -rf tool
    @rm -rf public/swagger
    @rm -rf public/schema
    @rm -rf public/js
    @rm -f todo

bootstrap:
    @goagen main    -d github.com/m0a-mystudy/goa-chat/design -o controllers

generate:
    @goagen app     -d github.com/m0a-mystudy/goa-chat/design
    @goagen swagger -d github.com/m0a-mystudy/goa-chat/design -o public
    @goagen schema  -d github.com/m0a-mystudy/goa-chat/design -o public
    @goagen client  -d github.com/m0a-mystudy/goa-chat/design
    @goagen js      -d github.com/m0a-mystudy/goa-chat/design -o public

build:
    @go build -o chat


DSLの記述

design/design.go
package design

import (
    . "github.com/goadesign/goa/design"
    . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("Chat API", func() {
    Title("goa study chat") // Documentation title
    Description("goa study chat api")
    Host("localhost:8080")
    Scheme("http")
    BasePath("/api")
    Origin("http://localhost:3000", func() {
        Methods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
        Headers("Origin", "X-Requested-With", "Content-Type", "Accept")
    })
    ResponseTemplate(Created, func(pattern string) {
        Description("Resource created")
        Status(201)
        Headers(func() {
            Header("Location", String, "href to created resource", func() {
                Pattern(pattern)
            })
        })
    })
})

var _ = Resource("room", func() {
    DefaultMedia(Room)
    BasePath("/rooms")
    Action("list", func() {
        Routing(GET(""))
        Description("Retrieve all rooms.")
        Response(OK, CollectionOf(Room))
        Response(NotFound)
    })

    Action("show", func() {
        Routing(
            GET("/:roomID"),
        )
        Description("Retrieve room with given id")
        Params(func() {
            Param("roomID", Integer)
        })
        Response(OK)
        Response(NotFound)
        Response(BadRequest, ErrorMedia)
    })

    Action("post", func() {
        Routing(POST(""))
        Description("Create new Room")
        Payload(RoomPayload)
        Response(Created, "/rooms/[0-9]+")
        Response(BadRequest)
    })

    Action("watch", func() {
        Routing(
            GET("/:roomID/watch"),
        )
        Scheme("ws")
        Description("Retrieve room with given id")
        Params(func() {
            Param("roomID", Integer)
        })
        Response(SwitchingProtocols)
        Response(BadRequest, ErrorMedia)
    })

})

var _ = Resource("message", func() {
    DefaultMedia(Message)
    BasePath("messages")
    Parent("room")
    Action("list", func() {
        Routing(GET(""))
        Description("Retrieve all messages.")
        Response(OK, CollectionOf(Message))
        Response(NotFound)
    })
    Action("post", func() {
        Routing(POST(""))
        Description("Create new message")
        Payload(MessagePayload)
        Response(Created, "^/rooms/[0-9]+/messages/[0-9]+$")
        Response(BadRequest)
    })

    Action("show", func() {
        Routing(
            GET("/:messageID"),
        )
        Description("Retrieve message with given id")
        Params(func() {
            Param("messageID", Integer)
        })
        Response(OK)
        Response(NotFound)
        Response(BadRequest, ErrorMedia)
    })

})

var Message = MediaType("application/vnd.message+json", func() {
    Description("A Message")
    Reference(MessagePayload)
    Attributes(func() {
        Attribute("accountID")
        Attribute("body")
        Attribute("postDate")
        Required("accountID", "body", "postDate")
    })
    View("default", func() {
        Attribute("accountID")
        Attribute("body")
        Attribute("postDate")
    })
})
var MessagePayload = Type("MessagePayload", func() {

    Attribute("accountID", Integer, func() {
        Example(1)
    })
    Attribute("body", func() {
        MinLength(1)
        MaxLength(400)
        Example("this is chat message")
    })
    Attribute("postDate", DateTime, func() {
        Default("1978-06-30T10:00:00+09:00")
    })

    Required("accountID", "body", "postDate")
})

var Room = MediaType("application/vnd.room+json", func() {
    Description("A room")
    Reference(RoomPayload)
    Attributes(func() {
        Attribute("id")
        Attribute("name")
        Attribute("description")
        Attribute("created")
        Required("name", "description")
    })

    View("default", func() {
        Attribute("id")
        Attribute("name")
        Attribute("description")
        Attribute("created")

    })
})

var RoomPayload = Type("RoomPayload", func() {
    Attribute("id", Integer, "ID of room")
    Attribute("name", String, "Name of room", func() {
        Example("room001")
    })
    Attribute("description", String, "description of room", func() {
        Example("room description")
        MaxLength(400)
    })
    Attribute("created", DateTime, "Date of creation")
    Required("name", "description")
})

上記はまだpayloadの作りとかよくわかってないのでちょっと仮な感じです。
ちょっとずつこちらを修正していきます。

詰まったところ

CORS設定

まずクライアントはcreate-react-appを使って作ろうとしてまして、何も考えないと別ドメイン扱いになります。
それでもアクセス可能にするためにCORSの設定が必要でしたがどこにも情報がなくて苦労しました。
(Content-Type is not allowed by Access-Control-Allow-Headersとか出る)
以下のように設定したら上手く動きました。

    Origin("http://localhost:3000", func() {
        Methods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
        Headers("Origin", "X-Requested-With", "Content-Type", "Accept")
    })

参考:http://stackoverflow.com/questions/5027705/error-in-chrome-content-type-is-not-allowed-by-access-control-allow-headers

親子関係の設定

methods エンドポイント 目的
get,post http://chat/api/rooms/3/messages room 3へのチャットメッセージの作成と一覧の取得

上記のように2つのリソース roomとmessageには親子関係になっているので
それを素直に設定するとこんな感じになります

var _ = Resource("room", func() {
    DefaultMedia(Room)
    BasePath("/rooms")

    Action("list", func() {
        Routing(GET(""))
        //省略
    })

    Action("post", func() {
        Routing(POST(""))
        //省略
    })

})

var _ = Resource("message", func() {
    DefaultMedia(Message)
    BasePath("messages")
    Parent("room")

    Action("list", func() {
        Routing(GET(""))
        //省略
    })
    Action("post", func() {
        Routing(POST(""))
        //省略
    })

})

ところがこのままdslからコード生成を行うと以下のようなエラーが出ます

exit status 1
resource "message": Parent resource "room" has no canonical action
link "room" of type "Message": Link name must match one of the parent media type attribute names
make: *** [bootstrap] Error 1

このエラーメッセージに悩みました。
対応方法はshowactionを親側に定義します。(canonical action)

roomリソースに以下のactionを追加
    Action("show", func() {
        Routing(
            GET("/:roomID"),
        )
        Description("Retrieve room with given id")
        Params(func() {
            Param("roomID", Integer)
        })
        Response(OK)
        Response(NotFound)
        Response(BadRequest, ErrorMedia)
    })

参考:https://goa.design/reference/goa/design/apidsl/#func-resource-a-name-apidsl-resource-a

(ちなみにshowという名前が嫌ならCanonicalActionNameを使って変更できる)

実装

make bootstrapを実行してコード生成を行います。
修正が必要なファイルはcontrollersに作られますが、main.goだけは手動で直下に移動しておきます。

database側を作る

goaをつかってdbのスキーマを作るのもいいのですが今回は
mysql側でsqlを書いてxoでgoの構造体を作るとう言う方針で行きます。

xoのインストール

$ go get -u -v github.com/knq/xo...

mysql側でスキーマを作りました。ダンプは以下となります。

--
-- Table structure for table `messages`
--

DROP TABLE IF EXISTS `messages`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `messages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `room_id` int(11) NOT NULL,
  `account_id` int(11) NOT NULL,
  `body` varchar(400) NOT NULL,
  `postDate` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `room_id_idx` (`room_id`),
  CONSTRAINT `room_id` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;


--
-- Table structure for table `rooms`
--

DROP TABLE IF EXISTS `rooms`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `rooms` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(256) NOT NULL,
  `description` varchar(400) NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name_UNIQUE` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;


mysqlへ流し込んでおきます。(方法は省略)
db_nameはgoa_chatとします。

以下のコマンドでdbスキーマからgo codeを生成します。

$ mkdir -p models
$ xo  mysql://<usrname>:<pass>@localhost/goa_chat -o models
$ ls -l models       
total 64
-rw-r--r--  1 m0a  staff  4199  5 17 13:25 message.xo.go
-rw-r--r--  1 m0a  staff  3581  5 17 13:25 room.xo.go
-rw-r--r--  1 m0a  staff  2128  5 17 10:56 xo_db.xo.go

生成されたコードはこんな感じです

models/room.xo.go
// Package models contains the types for schema 'goa_chat'.
package models

// GENERATED BY XO. DO NOT EDIT.

import (
    "errors"
    "time"
)

// Room represents a row from 'goa_chat.rooms'.
type Room struct {
    ID          int       `json:"id"`          // id
    Name        string    `json:"name"`        // name
    Description string    `json:"description"` // description
    Created     time.Time `json:"created"`     // created

    // xo fields
    _exists, _deleted bool
}

// Exists determines if the Room exists in the database.
func (r *Room) Exists() bool {
    return r._exists
}

// Deleted provides information if the Room has been deleted from the database.
func (r *Room) Deleted() bool {
    return r._deleted
}

// Insert inserts the Room to the database.
func (r *Room) Insert(db XODB) error {
    var err error

    // if already exist, bail
    if r._exists {
        return errors.New("insert failed: already exists")
    }

    // sql insert query, primary key provided by autoincrement
    const sqlstr = `INSERT INTO goa_chat.rooms (` +
        `name, description, created` +
        `) VALUES (` +
        `?, ?, ?` +
        `)`

    // run query
    XOLog(sqlstr, r.Name, r.Description, r.Created)
    res, err := db.Exec(sqlstr, r.Name, r.Description, r.Created)
    if err != nil {
        return err
    }

    // retrieve id
    id, err := res.LastInsertId()
    if err != nil {
        return err
    }

    // set primary key and existence
    r.ID = int(id)
    r._exists = true

    return nil
}

//
//     コード省略
//

// RoomByID retrieves a row from 'goa_chat.rooms' as a Room.
//
// Generated from index 'rooms_id_pkey'.
func RoomByID(db XODB, id int) (*Room, error) {
    var err error

    // sql query
    const sqlstr = `SELECT ` +
        `id, name, description, created ` +
        `FROM goa_chat.rooms ` +
        `WHERE id = ?`

    // run query
    XOLog(sqlstr, id)
    r := Room{
        _exists: true,
    }

    err = db.QueryRow(sqlstr, id).Scan(&r.ID, &r.Name, &r.Description, &r.Created)
    if err != nil {
        return nil, err
    }

    return &r, nil
}

中身を見ると全体を取得する関数が定義されてないのでroomの分だけ作ります

models/room.go
package models

func AllRooms(db XODB, limit int) ([]*Room, error) {

    // sql query
    const sqlstr = `SELECT ` +
        `id, name, description, created ` +
        `FROM goa_chat.rooms LIMIT ?`
    // run query
    XOLog(sqlstr, limit)
    q, err := db.Query(sqlstr, limit)
    if err != nil {
        return nil, err
    }
    defer q.Close()

    var res []*Room
    for q.Next() {
        r := Room{}
        err = q.Scan(&r.ID, &r.Name, &r.Description, &r.Created)
        if err != nil {
            return nil, err
        }
        res = append(res, &r)
    }
    return res, nil
}

goaのコードとdbの紐付け

goa-cellerを真似てcontrollerにdbをつけておきます。

controllersroom.go,message.goを修正します

controllers/room.go
// RoomController implements the room resource.
type RoomController struct {
    *goa.Controller
+   db *sql.DB
}

// NewRoomController creates a room controller.
- func NewRoomController(service *goa.Service) *RoomController {
+ func NewRoomController(service *goa.Service, db *sql.DB) *RoomController {
    return &RoomController{
        Controller: service.NewController("RoomController"),
+       db:         db,
    }
}

message.goは省略します

main.goにて実際にdbを接続するコードを追加します

main.go
package main

import (
+   "database/sql"

    "github.com/goadesign/goa"
    "github.com/goadesign/goa/middleware"
    "github.com/m0a-mystudy/goa-chat/app"
    "github.com/m0a-mystudy/goa-chat/controllers"

+   _ "github.com/go-sql-driver/mysql"
)

func main() {
    // Create service
    service := goa.New("Chat API")

    // Mount middleware
    service.Use(middleware.RequestID())
    service.Use(middleware.LogRequest(true))
    service.Use(middleware.ErrorHandler(service, true))
    service.Use(middleware.Recover())

+   db, err := sql.Open("mysql", "user:password@/goa_chat?parseTime=true")
+   if err != nil {
+       service.LogError("startup", "err", err)
+   }
    // Mount "message" controller
-   c := controllers.NewMessageController(service)
+   c := controllers.NewMessageController(service, db)
    app.MountMessageController(service, c)
    // Mount "room" controller
-   c2 := controllers.NewRoomController(service)
+   c2 := controllers.NewRoomController(service, db)
    app.MountRoomController(service, c2)

    // Start service
    if err := service.ListenAndServe(":8080"); err != nil {
        service.LogError("startup", "err", err)
    }
}

controllers/room.go実装

先ずはmodel側の構造体とgoa側の構造体の変換用の処理関数を作っておきます

func ToRoomMedia(room *models.Room) *app.Room {
    ret := app.Room{
        ID:          &room.ID,
        Description: room.Description,
        Name:        room.Name,
        Created:     &room.Created,
    }
    return &ret
}

DSLでActionを定義した分だけメソッドができているので中味を実装していきます。

package controllers

import (
    "database/sql"
    "time"

    "github.com/goadesign/goa"
    "github.com/m0a-mystudy/goa-chat/app"
    "github.com/m0a-mystudy/goa-chat/models"
)


//
//     コード省略
//

// List runs the list action.
func (c *RoomController) List(ctx *app.ListRoomContext) error {
    res := app.RoomCollection{}
    rooms, err := models.AllRooms(c.db, 100) //とりあえず100件固定で
    if err != nil {
        return err
    }
    for _, room := range rooms {
        res = append(res, ToRoomMedia(room))
    }
    return ctx.OK(res)
}

// Post runs the post action.
func (c *RoomController) Post(ctx *app.PostRoomContext) error {
    room := models.Room{
        Name:        ctx.Payload.Name,
        Description: ctx.Payload.Description,
        Created:     time.Now(),
    }
    err := room.Insert(c.db)
    if err != nil {
        return err
    }
    return ctx.Created(ToRoomMedia(&room))
}

// Show runs the show action.
func (c *RoomController) Show(ctx *app.ShowRoomContext) error {
    room, err := models.RoomByID(c.db, ctx.RoomID)
    if err != nil {
        return err
    }
    if room == nil {
        return ctx.NotFound()
    }
    res := ToRoomMedia(room)
    return ctx.OK(res)
}

ctxからroomIDが取得できたり、Actionのルーティング設定から必要なパラメータをctxから取得できます。
便利。フレームワークによってはinterface{}型だったりするんですがコード生成なのでちゃんとroomIDはint型になっているのが素敵です。

controllers/message.go実装

こちらも基本的に同じです。

package controllers

import (
    "database/sql"
    "time"

    "github.com/goadesign/goa"
    "github.com/m0a-mystudy/goa-chat/app"
    "github.com/m0a-mystudy/goa-chat/models"
)

//
//     コード省略
//


// List runs the list action.
func (c *MessageController) List(ctx *app.ListMessageContext) error {
    res := app.MessageCollection{}

    messages, err := models.MessagesByRoomID(c.db, ctx.RoomID)
    if err != nil {
        return err
    }
    for _, m := range messages {
        res = append(res, ToMessageMedia(m))
    }
    return ctx.OK(res)
}

// Post runs the post action.
func (c *MessageController) Post(ctx *app.PostMessageContext) error {
    m := models.Message{
        RoomID:    ctx.RoomID,
        AccountID: ctx.Payload.AccountID,
        Body:      ctx.Payload.Body,
        Postdate:  time.Now(),
    }

    err := m.Insert(c.db)
    if err != nil {
        return ctx.BadRequest()
    }

    return ctx.Created(ToMessageMedia(&m))
}

xoもスキーマから読み取ってMessagesByRoomIDを作ってくれるのが素敵です。

WebScoket処理の実装

チャットなのでRoom毎に変更通知を受け取るようにします。
websocketのコネクションごとに作るチャネルをプールする実装を作ります

controllers/websocket.go
package controllers
import (
    "context"
    "github.com/goadesign/goa"
)

var (
    loginfo  = goa.LogInfo
    logError = goa.LogError
)


type Comm chan struct{}

// WsConnections is connection pool websocket conn
type WsConnections struct {
    connections map[int][]Comm
    ctx         context.Context
}

// NewConnections create WsConnections
func NewConnections(ctx context.Context) *WsConnections {
    return &WsConnections{
        connections: map[int][]Comm{},
        ctx:         ctx,
    }
}

func (l *WsConnections) apendConn(roomID int, comm Comm) {
    list := l.connections[roomID]
    if list == nil {
        list = []Comm{}
    }
    list = append(list, comm)
    l.connections[roomID] = list
    loginfo(l.ctx, "apendConn", "list", list)
}

func (l *WsConnections) removeConn(roomID int, comm Comm) {
    list := l.connections[roomID]
    if list == nil {
        list = []Comm{}
    }
    newList := []Comm{}
    for _, c := range list {
        if c == comm {
            continue
        }
        newList = append(newList, c)
    }

    l.connections[roomID] = newList
    loginfo(l.ctx, "removeConn", "list", newList)
}

func (l *WsConnections) updateRoom(roomID int) {
    loginfo(l.ctx, "updateRoom", "roomID", roomID)
    comms, ok := l.connections[roomID]
    if !ok {
        return
    }
    loginfo(l.ctx, "updateRoom", "comms", comms)
    for _, comm := range comms {
        comm <- struct{}{}
    }
}

dbと同じようcontrolerから管理します

controllers/message.go
// MessageController implements the message resource.
type MessageController struct {
    *goa.Controller
    db          *sql.DB
+   connections *WsConnections
}

-func NewMessageController(service *goa.Service, db *sql.DB) *MessageController {
+func NewMessageController(service *goa.Service, db *sql.DB, wsc *WsConnections) *MessageController {
    return &MessageController{
        Controller:  service.NewController("MessageController"),
        db:          db,
+       connections: wsc,
    }
}

controllers/message.goとmain.goの修正は省略します。dbの時と同じなので
websocketによるコネクションが張られたら上記プールにチャネルを登録する処理を実装します

controllers/room.go
// Watch watches the message with the given id.
func (c *RoomController) Watch(ctx *app.WatchRoomContext) error {
    Watcher(ctx.RoomID, c, ctx).ServeHTTP(ctx.ResponseWriter, ctx.Request)
    return nil
}

// Roomの変更通知の送信
func Watcher(roomID int, c *RoomController, ctx *app.WatchRoomContext) websocket.Handler {
    return func(ws *websocket.Conn) {
        ch := make(chan struct{})
        c.connections.apendConn(roomID, ch)
        for {
            <-ch
            _, err := ws.Write([]byte(fmt.Sprintf("Room: %d", roomID)))
            if err != nil {
                break
            }
        }
        c.connections.removeConn(roomID, ch)
    }
}

メッセージがポストされるたびに通知を飛ばすように実装を追加します

controllers/message.go
// Post runs the post action.
func (c *MessageController) Post(ctx *app.PostMessageContext) error {
    m := models.Message{
        RoomID:    ctx.RoomID,
        AccountID: ctx.Payload.AccountID,
        Body:      ctx.Payload.Body,
        Postdate:  time.Now(),
    }

    err := m.Insert(c.db)
    if err != nil {
        //return err
        return ctx.BadRequest()
    }
+   c.connections.updateRoom(ctx.RoomID)
    ctx.ResponseData.Header().Set("Location", app.MessageHref(ctx.RoomID, m.ID))
    return ctx.Created()
}

クライアント側の実装

自分はtypeScriptが好きなのでreact+typescriptな環境を作ります。

swaggerからclient apiを作るための環境をインストールします

$ brew install swagger-codegen

Makefile等を変更します

all: depend clean generate generete-client build

clean:
    @rm -rf app
    @rm -rf client
    @rm -rf tool
    @rm -rf public/swagger
    @rm -rf public/schema
    @rm -rf public/js
    @rm -f todo

bootstrap:
    @goagen main    -d github.com/m0a-mystudy/goa-chat/design -o controllers

generate:
    @goagen app     -d github.com/m0a-mystudy/goa-chat/design
    @goagen swagger -d github.com/m0a-mystudy/goa-chat/design -o public
    @goagen schema  -d github.com/m0a-mystudy/goa-chat/design -o public
    @goagen client  -d github.com/m0a-mystudy/goa-chat/design
    @goagen js      -d github.com/m0a-mystudy/goa-chat/design -o public

generate-client:
    @swagger-codegen generate -l typescript-fetch -i ./public/swagger/swagger.json -o ./chat-client-api
    @jq -s '.[0] * .[1]' chat-client-api/package.json chat-client-api/package_replace.json > replaced_package.json
    @rm chat-client-api/package.json
    @mv replaced_package.json chat-client-api/package.json

build:
    @go build -o chat

build-client:
    @cd chat-client-api && yarn
    @cd chat-client-api && npm link


run:
    @chat

また Makefileと合わせて以下も追加します

chat-client-api/package_replace.json

{
  "name": "chat-client-api",
  "version": "0.0.1",
  "license": "MIT"
}

上記修正後に

make generate-clientmake build-clientを実行してapiクライアントを自動生成し npm linkしておきます。

合わせて以下のコマンドでReact環境も作ります

$ create-react-app --scripts-version=react-scripts-ts  goa-chat-client
$ cd goa-chat-client
$ npm link chat-client-api
$ yarn 
$ yarn start

localhost:3000でクライアントが立ち上がります。

以下実装内容の抜粋になります

goa-chat-client/src/Chat.tsx
import * as React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import * as comm from 'chat-client-api';

type ChatProps = RouteComponentProps<{ roomID: number }>;
interface ChatState {
    messages: comm.MessageCollection;
    text: string;
}
export default class Chat
    extends React.Component<ChatProps, ChatState> {

    private messageAPI: comm.MessageApi;
    constructor(props: ChatProps) {
        super(props);
        this.state = {
            messages: [] as comm.MessageCollection,
            text: ''
        };
        this.messageAPI = new comm.MessageApi();
        this.fetchMessages.bind(this);
        this.onChangeText.bind(this);
        this.postMessage.bind(this);
    }

    async fetchMessages() {
        const roomID = this.props.match.params.roomID;
        const messages = await this.messageAPI.messageList({ roomID });
        this.setState({
            messages
        });
    }

    async postMessage() {
        const accountID = 10;
        const body = this.state.text;
        const roomID = this.props.match.params.roomID;
        const options = { 
            mode: 'cors',
            // credentials: 'include',
            headers: {
                'content-Type': 'application/json',
                'accept' : 'application/vnd.message+json'
            }
        } as {};
        const payload = {
            accountID,
            body
        } as comm.MessagePayload;
        await this.messageAPI.messagePost({
            roomID,
            payload
        },                                options);
        await this.fetchMessages();

    }
    async componentDidMount() {
        await this.fetchMessages();

        const roomID = this.props.match.params.roomID;
        const wsURL = `ws://localhost:8080/api/rooms/${roomID}/watch`;
        const ws = new WebSocket(wsURL);

        ws.onmessage = async (ev) => {
            await this.fetchMessages();
        };
    }

    onChangeText(e: {target: { value: string}}) {
        this.setState({ text: e.target.value });
    }

    render() {
        const { messages, text } = this.state;
        return (
            <div>
                {messages.map(message => {
                    return (
                        <div key={`postDate=${message.postDate}`} >
                            <p>id:{message.accountID}</p>
                            <p>{message.body}</p>
                            <p>postDate:{message.postDate}</p>
                        </div>
                    );
                })}
                <textarea value={text} onChange={e => (this.onChangeText(e))} />
                <button onClick={() => (this.postMessage())}> submit </button>
            </div>);
    }
}

最後に

swagger.yamlが作られているので
https://editor.swagger.io/ から対応するcurlコマンドを作ってくれたり
typescriptのクライアントapiを自動生成してくれるのが素敵です。

とりあえずここまでのコードは以下においておきます。
https://github.com/m0a-mystudy/goa-chat

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away