【REST編】React+Djangoで開発するWeb API

Web APIにはRESTやGraghQL、gRPCがあるけどどう使い分ければ良いだろう?

Web APIとして広く知られているのはRESTですが、GraphQLgRPCなどの案件も見かけるようになりました

ですが、これまでRESTしか使ってこなかったエンジニアにとってどのように使い分ければ良いのか分かりません…

そこでフロントエンドにReact、バックエンドにDjangoを採用した環境下でREST、GraphQL、gRPCの実装方法と使い分けについて解説していきます

初回はRESTの実装方法についてです

前提条件
  • Visual Studio Code:1.76.0
  • Docker Desktop

ReactプロジェクトはCreate React Appで作成しており、TypeScriptテンプレートを使用しています

詳しくはこちらの記事を参照してください

目次

DjangoのREST framework

まずはDjangoのプロジェクト側で作業します

次のコマンドでDjangoにREST frameworkをインストールします

pip install djangorestframework

プロジェクトにアプリケーションを追加します。今回はアプリケーション名をapiとしました

python manage.py startapp api

settings.pyに先ほど追加したREST frameworkとアプリケーションの登録を行います

自動生成されるConfigクラスの名前はapps.pyを確認してください

INSTALLED_APPS = [
    ...
    'rest_framework',
    'api.apps.ApiConfig', # アプリケーション名によってxxx.apps.XxxConfigとなる
]

models.pyにこのアプリケーションで使用するModel(テーブル)を追加します

# Create your models here.
class Todo(models.Model):
    task = models.CharField(verbose_name='タスク', max_length=180)
    timestamp = models.DateTimeField(
        verbose_name='タイムスタンプ', auto_now_add=False)
    completed = models.BooleanField(verbose_name='完了済み')

    class Meta:
        verbose_name_plural = 'Todo'

作成したModelをJSONなどのAPIに適した形式に変換するためにserializers.pyファイルを作成します

from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('task', 'timestamp', 'completed')

views.pyにModelからデータを取得して返却するAPIViewを作成します

from .serializers import TodoSerializer
from .models import Todo
from rest_framework import generics

# Create your views here.
class TodoAPIView(generics.ListAPIView):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

APIのエンドポイントを設定するためにurls.pyファイルを作成します

from django.urls import path
from .views import TodoAPIView

urlpatterns = [
    path('todo', TodoAPIView.as_view()),
]

urls.pyにルーティングがプロジェクトのurls→作成したアプリケーションのurlsになるように設定します

from django.urls import path, include

urlpatterns = [
    ...
    path("api/", include("api.urls")), # アプリケーション名によってxxx.urlsとなる
]

新しくテーブルを作成したのでマイグレーションを実行します

python manage.py makemigrations
python manage.py migrate

admin.pyに追加したテーブルが管理画面で使えるように設定します

from .models import Todo

# Register your models here.
admin.site.register(Todo)

サーバを起動してデータ登録を行います

python manage.py runserver

管理画面から作成したTodoの追加を選択します

管理画面のTodoテーブル

React側で表示するデータを2件ほど登録します

管理画面からTodoのデータ登録

Reactのaxios

続いてReactのプロジェクト側で作業をします

次のコマンドで非同期でAPI通信ができるようにaxiosをインストールします

npm install axios

Djangoで作成したAPIから取得した結果をテーブルに出力するようにします

  const axiosInstance = axios.create({
    baseURL: 'http://localhost:8000',
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
    },
  });

  const [tableData, setTableData] = useState<Todo[]>([]);

  useEffect(() => {
    axiosInstance.get('/api/todo').then((response) => {
      setTableData(response.data);
    });
  }, []);

。全ソースはまとめを参照してください

useStateとは?

useStateとはレンダリングに影響するHookで、現在の状態とそれを更新する関数の2つを配列で返します。初期値が引数となり、setter関数が呼び出されるたびに再レンダリングのトリガーになります

useEffectとは?

useEffectはレンダリングに影響しないHook副作用(side-effect)を管理するために使用されます。レンダリングの外部でコンポーネントに影響を与える操作としてAPI通信やタイマー設定などがあります

TypeScriptでは型を指定しないとエラーになるためAPIの返却値であるTodoを定義します

export interface Todo {
  task: string;
  timestamp: string;
  completed: bool;
}

DJangoが起動している状態でReactも起動します

npm start

VSCodeはDjangoで使用しているので別のコンソール(Powershellなど)を立ち上げればReactも起動できますよ

CORSエラーになる?

正常に通信ができた場合でもCORSエラーが発生し、画面に結果が表示されないと思います

DevToolsのConsoleにCORSエラーが出力

Django側で特定のIPに対してCORSを許可するようにするため、次のパッケージをインストールします

pip install django-cors-headers

settings.pyにアプリケーションとミドルウェアの登録、CORSエラーとしないIPアドレスを追加します

INSTALLED_APPS = [
    ...
    "corsheaders",
]

MIDDLEWARE = [
    ...
    "corsheaders.middleware.CorsMiddleware"
]

# React側のIPアドレスを追加
CORS_ORIGIN_WHITELIST = [
    'http://localhost:3000',
    'http://127.0.0.1:3000',
]

Djangoサーバーを再起動すれば正常に通信ができるようになっています

APIから取得した結果をテーブルに表示

まとめ

今回作成したReact側のソースです

App.tsx
import { Card, CardContent, Container } from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import * as React from 'react';
import CustomTable from './components/CustomTable';

function App() {
  const theme = createTheme({});
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container fixed>
        <Card variant="elevation" elevation={0}>
          <CardContent>
            <CustomTable />
          </CardContent>
        </Card>
      </Container>
    </ThemeProvider>
  );
}

export default App;
CustomTable.tsx
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { styled } from '@mui/material/styles';
import axios from 'axios';
import React, { useState, useEffect } from 'react';
import { Todo } from '../global';

function CustomTable() {
  const axiosInstance = axios.create({
    baseURL: 'http://localhost:8000',
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
    },
  });

  const [tableData, setTableData] = useState<Todo[]>([]);

  useEffect(() => {
    axiosInstance.get('/api/todo').then((response) => {
      setTableData(response.data);
    });
  }, []);

  const StyledTableCell = styled(TableCell)(({ theme }) => ({
    [`&.${tableCellClasses.head}`]: {
      backgroundColor: theme.palette.common.black,
      color: theme.palette.common.white,
    },
    [`&.${tableCellClasses.body}`]: {
      fontSize: 14,
    },
  }));

  return (
    <TableContainer component={Paper}>
      <Table sx={{ minWidth: 650 }} aria-label="simple table">
        <TableHead>
          <TableRow>
            <StyledTableCell>Task</StyledTableCell>
            <StyledTableCell>Timestamp</StyledTableCell>
            <StyledTableCell>Completed</StyledTableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {tableData.map((row) => (
            <TableRow key={row.task}>
              <TableCell>{row.task}</TableCell>
              <TableCell>{row.timestamp}</TableCell>
              <TableCell>{row.completed.toString()}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

export default CustomTable;
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次