😎
Docker×FastAPI×React(TypeScript) on AWS ECS【frontend】
前回のbackend編からの続きです
backend編
正直、フロント側はさっぱりセンスが無い+忘れまくってたりしたのでめちゃくちゃ調べ直した。
あらためて両方できる皆さんすごいわぁ、、(ちなみに僕はバックエンド側もセンスないです)
frontend側に入る前に前回やった内容がちょっと半端だったのでまずはそこをサクッと修正してしまおうと思います。
crud.py
# project/backend/app/api/crud.py
from app.models.pydantic import SummaryPayloadSchema
from app.models.tortoise import TextSummary
from typing import Union, Listiter
async def post(payload: SummaryPayloadSchema) -> int:
summary = TextSummary(
summary=payload.summary,
)
await summary.save()
return summary.id
async def get_all() -> List:
summarys = await TextSummary.all().values()
return summarys
async def get(id: int) -> Union[dict, None]:
task = await TextSummary.filter(id=id).first().values()
if task:
return task[0]
return None
async def put(id: int, payload: SummaryPayloadSchema) -> Union[dict, None]:
task = await TextSummary.filter(id=id).update(
summary=payload.summary
)
if task:
update_task = await TextSummary.filter(id=id).first().values()
return update_task[0]
return None
async def delete(id: int) -> int:
task = await TextSummary.filter(id=id).first().delete()
return task
main.py
# project/backend/app/main.py
import logging
import os
from fastapi import FastAPI, HTTPException, Path
from typing import List
from app.api import crud
from app.db import init_db
from fastapi.middleware.cors import CORSMiddleware
from app.models.tortoise import SummarySchema
from app.models.pydantic import SummaryPayloadSchema, SummaryResponseSchema
log = logging.getLogger("uvicorn")
app = FastAPI()
origins = [
"*",
"localhost:3000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
@app.on_event("startup")
async def startup_event():
log.info("Starting up...")
init_db(app)
@app.on_event("shutdown")
async def shutdown_event():
log.info("Shutting down...")
@app.post("/todos", response_model=SummaryResponseSchema, status_code=201)
async def create_summary(payload: SummaryPayloadSchema) -> SummaryResponseSchema:
summary_id = await crud.post(payload)
response_object = {
"id": summary_id,
"summary": payload.summary
}
return response_object
@app.get("/todos", response_model=List[SummarySchema])
async def read_all_todo() -> List[SummarySchema]:
return await crud.get_all()
@app.put("/todos/{id}/", response_model=SummarySchema)
async def update_todo(payload: SummaryPayloadSchema, id: int = Path(..., gt=0)) -> SummarySchema:
todo = await crud.put(id, payload)
if not todo:
raise HTTPException(status_code=404,detail="Todos not found")
return todo
@app.delete("/todos/{id}/", response_model=SummaryResponseSchema)
async def delete_todo(id: int = Path(..., gt=0)) -> SummaryResponseSchema:
todo = await crud.get(id)
if not todo:
raise HTTPException(status_code=404, detail="Todos not found")
await crud.delete(id)
return todo
更新と削除用のエンドポイントを追加したのでこれで一旦CRUDの機能を確認しましょう。
確認出来たら一回止めときます。
docker-compose stop
そんじゃあ、frontend側いってみよー
frontendディレクトリを作成して、Dockerfileを用意。
mkdir frontend && cd frontend
touch Dockerfile
# project/frontend/Dockerfile
#pull official base-image
FROM node:16-alpine
WORKDIR /usr/src/
docker-composeも修正していきます。
version: '3.8'
services:
web:
build:
context: ./project/backend
dockerfile: Dockerfile
command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000
volumes:
- ./project/backend:/usr/src/
environment:
- DATABASE_URL=postgres://postgres:postgres@web-db:5432/develop
ports:
- 8004:8000
depends_on:
- web-db
web-db:
build:
context: ./project/backend/db
dockerfile: Dockerfile
expose:
- 5432
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
front:
stdin_open: true
build:
context: ./project/frontend
dockerfile: Dockerfile
volumes:
- ./project/frontend:/usr/src/
ports:
- 3007:3000
depends_on:
- web
修正後に以下のコマンドを入力します。
docker-compose build
docker-compose run front sh -c "npx create-react-app app --template typescript"
プロンプトが返ってきたら更に以下を実行しましょう。
docker-compose up -d
コンテナが起動していることを確認後、frontのコンテナに入ります。
docker ps
docker-compose exec front sh
コンテナに入ったらプロジェクトのディレクトリに移動して、ChakraUIをインストールします。
ChakraUI
cd app
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
終わったらコンテナから出て、Dockerfileとdocker-compose.ymlを再度編集します。
#pull official base-image
FROM node:16-alpine
WORKDIR /usr/src/app/
#add usr/src/app/node_modules/.bin to $PATH
ENV PATH /usr/src/app/node_modules.bin:$PATH
##install and cache app dependencies
COPY app/package.json .
COPY app/package-lock.json .
#npm install
RUN npm ci
RUN npm install react-scripts@4.0.3 -g --silent
#
#start app
CMD ["npm", "start"]
version: '3.8'
services:
web:
build:
context: ./project/backend
dockerfile: Dockerfile
command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000
volumes:
- ./project/backend:/usr/src/
environment:
- DATABASE_URL=postgres://postgres:postgres@web-db:5432/develop
ports:
- 8004:8000
depends_on:
- web-db
web-db:
build:
context: ./project/backend/db
dockerfile: Dockerfile
expose:
- 5432
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
front:
stdin_open: true
build:
context: ./project/frontend
dockerfile: Dockerfile
volumes:
- ./project/frontend:/usr/src/
- /usr/src/app/node_modules
ports:
- 3007:3000
depends_on:
- web
編集後は再度コンテナを立ち上げて、以下のリンク先に飛んだらreactのいつもの画面が確認できます。
docker-compose up -d --build
っしゃあ、画面つくってくぞぉ
まずはグローバルスタイルを設定してしまいます。
mkdir project/frontend/app/src/theme
touch project/frontend/app/src/theme/theme.ts
#project/frontend/app/src/theme/theme.ts
import { extendTheme } from "@chakra-ui/react";
const theme = extendTheme({
styles:{
global: {
body: {
backgroundColor: "gray.700",
color: "teal.400",
}
}
}
});
export default theme;
お次は型定義用のファイルを作ります。
mkdir project/frontend/app/src/types && mkdir project/frontend/app/src/types/api
touch project/frontend/app/src/types/api/TodoType.ts
export type TodoType = {
"id": number;
"summary": string;
"created_at": string;
}
続けてComponentという名ばかりのファイルを作ります。
言い訳になりますが、はやく本題のECSの記事を書きたいので1つにまとめてしまいました。
はい。言い訳です。
mkdir project/frontend/app/src/Components
touch project/frontend/app/src/Components/IndexPage.tsx
import React, {ChangeEvent, memo, useCallback, useEffect, useState, ReactNode, VFC} from "react";
import {
Box,
Button,
Divider,
Flex,
Heading,
Input,
Stack,
Text,
Modal,
ModalOverlay,
ModalContent,
useDisclosure, ModalHeader, ModalCloseButton, ModalBody, FormControl, FormLabel, ModalFooter, FormHelperText,
} from "@chakra-ui/react";
import { TodoType } from "../types/api/TodoType";
export const IndexPage: VFC = memo(() => {
const [todoName, setTodoName] = useState('');
const [todos, setTodos] = useState<Array<TodoType>>([]);
const onChangeTask = (e: ChangeEvent<HTMLInputElement>) => setTodoName(e.target.value);
const getTodos = useCallback(async() => {
const response: Response = await fetch("http://localhost:8004/todos")
await response.json()
.then((r) => {
setTodos(r)
})
.catch(() => {
alert("undefined get Reasponse...")
});
},[]);
const onClickAddTodo = () => {
if (todoName === "") return;
const newTodo = {
"id": todos.length + 1,
"summary": todoName
}
fetch("http://localhost:8004/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTodo)
}).then(() => {
getTodos();
}).catch(() => {
alert("unknown posted error")
})
setTodoName("");
};
useEffect( () => {
getTodos();
},[getTodos]);
return (
<Flex
align="center"
color="gray.500"
height="100vh"
justify="center"
>
<Box
w="400">
<Stack spacing={6}>
<Heading
as="h1"
size="lg"
textAlign="center"
>TodoApp
</Heading>
<Box w={400}>
<Stack>
<Input
placeholder="Input taskname.."
value={todoName}
onChange={onChangeTask}
/>
<Button
colorScheme="teal"
onClick={onClickAddTodo}
>Add</Button>
<Divider my={"5"}/>
</Stack>
</Box>
{todos.map((todo) => (
<Todo
key={todo.summary}
id={todo.id}
item={todo.summary}
getTodos={getTodos}
/>
))}
</Stack>
</Box>
</Flex>
)
});
type Props = {
key: string;
id: number;
item: string;
getTodos: () => void;
}
const Todo: VFC<Omit<Props, "key">> = memo((props) => {
const { id, item, getTodos } = props;
return (
<>
<Box>
<Text>{item}</Text>
<UpdateTodo
id={id}
item={item}
getTodos={getTodos}
>Update</UpdateTodo>
<DeletedTodo
id={id}
item={item}
getTodos={getTodos}
>Delete</DeletedTodo>
</Box>
</>
);
});
type UpdateProps = {
id: number;
item: string;
getTodos: () => void;
children: ReactNode;
}
const UpdateTodo: VFC<UpdateProps> = (props) => {
const { id, item, getTodos, children } = props;
const [ todo, setTodo ] = useState(item);
const { isOpen, onOpen, onClose } = useDisclosure();
const onClickModalOpen = useCallback(() => onOpen(), [])
const onChangeTodo = (e: ChangeEvent<HTMLInputElement>) => setTodo(e.target.value);
const onClickUpdateTodo = async(id: number, item: string) => {
await fetch(`http://localhost:8004/todos/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ summary: item })
})
onClose();
getTodos();
};
return (
<>
<Modal
isOpen={isOpen}
onClose={onClose}
autoFocus={false}
>
<ModalOverlay>
<ModalContent>
<ModalHeader>Task Update</ModalHeader>
<ModalCloseButton/>
<ModalBody>
<Stack>
<FormControl>
<FormLabel>Task rename</FormLabel>
<Input
value={todo}
onChange={onChangeTodo}
></Input>
</FormControl>
</Stack>
</ModalBody>
<ModalFooter>
<Button onClick={() => onClickUpdateTodo(id, todo)}>Done</Button>
</ModalFooter>
</ModalContent>
</ModalOverlay>
</Modal>
<Button mr="2" onClick={onClickModalOpen}>{children}</Button>
</>
);
};
type DeletedProps = {
id: number;
item: string;
getTodos: () => void;
children: ReactNode;
}
const DeletedTodo: VFC<DeletedProps> = (props) => {
const { id, item, getTodos, children } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
const onClickModalOpen = useCallback(() => onOpen(), [])
const onClickDeletedTodo = async(id: number) => {
await fetch(`http://localhost:8004/todos/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" }
})
onClose();
getTodos();
};
return (
<>
<Modal
isOpen={isOpen}
onClose={onClose}
autoFocus={false}
>
<ModalOverlay>
<ModalContent>
<ModalHeader>Task Delete</ModalHeader>
<ModalCloseButton/>
<ModalBody>
<Stack>
<FormControl>
<FormLabel>this task delete?</FormLabel>
<FormHelperText>{item}</FormHelperText>
</FormControl>
</Stack>
</ModalBody>
<ModalFooter>
<Button onClick={() => onClickDeletedTodo(id)}>Done</Button>
</ModalFooter>
</ModalContent>
</ModalOverlay>
</Modal>
<Button onClick={onClickModalOpen}>{children}</Button>
</>
);
};
最後にApp.tsxを編集します。
import React from 'react';
import { IndexPage } from "./Components/IndexPage";
import {
ChakraProvider,
} from "@chakra-ui/react";
import theme from "./theme/theme";
function App() {
return (
<>
<header className="App-header">
<ChakraProvider theme={theme}>
<IndexPage/>
</ChakraProvider>
</header>
</>
);
}
export default App;
再度以下のリンク先に飛んでみます。
※コンパイルエラーとか出て消えないようであれば以下を入力して下さい。
docker-compose up -d --build
今はこんな感じになってるはず
現在のプロジェクト配下
.
├── docker-compose.yml
└── project
├── backend
│ ├── Dockerfile
│ ├── app
│ │ ├── __init__.py
│ │ ├── api
│ │ │ └── crud.py
│ │ ├── db.py
│ │ ├── main.py
│ │ └── models
│ │ ├── __init__.py
│ │ ├── pydantic.py
│ │ └── tortoise.py
│ ├── db
│ │ ├── Dockerfile
│ │ └── create.sql
│ ├── entrypoint.sh
│ └── requirements.txt
└── frontend
├── Dockerfile
└── app
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── Components
│ │ └── IndexPage.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ ├── setupTests.ts
│ ├── theme
│ │ └── theme.ts
│ └── types
│ └── api
│ └── TodoType.ts
├── tsconfig.json
└── yarn.lock
今回もお疲れさまでした!
ようやく次回AWSだぜ、、
Discussion