
フルスタックエンジニアの需要はスタートアップであればあるほど強いものです。そして、エンジニアにとってフロントエンドとバックエンドの両方を扱うことは楽しみでもあります。今回はよくあるReactとExpressを使ったアプリケーションを作成し、同時開発の方法を紹介します。
見出し
はじめに
スタートアップにおいて、フルスタックエンジニアの需要が高いのは理に適っています。なぜなら、フロントエンドエンジニアやバックエンドエンジニアという枠組みでエンジニアを採用してしまうと、単純に2倍のコストがかかるからです。そして、役割を分けるということはコミュニケーションコストの発生を防ぐことができません。仕事をしている人なら誰でも知っている話ですが、コミュニケーションコストが最も高く付きます。なぜ周りのマネージャーが仕事をしていないかのように見えるのか?なぜならコストの大半をコミュニケーションに費やしているからですね。私もマネジメントの仕事をしたことがありますが、正直、開発の仕事より何倍もストレスが多いです。
話が少し脱線してしまいましたが、今回は、JavascriptのフルスタックエンジニアとしてReactとExpressを同時に開発する事を想定して、基本的なサンプルアプリケーションを構築しながら、同時にフロントエンドとバックエンドを開発する環境構築方法を紹介していきます。IsomophicとかUniversalとかそういう話はしませんのあしからず。
MongoDBの準備
それでは、ReactとExpressの同時開発環境を作るために、まずは簡単ではありますが、フロントエンドをReact、バックエンドをExpressとMongoDBでサンプルアプリケーションを構築していきましょう。
MongoDBを起動する(Mac編)
今回は開発用のデータベースとしてMongoDBを使います。Macの場合は、Homebrewでインストールし、launchctlで常駐化させます。
$ brew install mongodb
$ ln -s /usr/local/opt/mongodb/homebrew.mxcl.mongodb.plist ~/Library/LaunchAgents
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mongodb.plist
$ mongo --version
MongoDB shell version v3.6.3
...
これでPCを起動するとMongoDBが自動的に立ち上がります。デフォルトのポートは「27017」です。
MongoDBを起動する(Docker編)
最近はなんでもDockerにするのが主流ですし、MacでもWindowsでも起動できるので普通に楽ですね。
一応ですが、Dockerをインストールしていない場合は、HomebrewからCask経由で簡単にできます。
$ brew cask install docker
$ docker --version
Docker version 17.12.0-ce, build c97c6d6
続いて、以下のコマンドを実行すればOKです。
$ docker container run -d --rm --name mongo-docker -p 27018:27017 mongo:3.6.3
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
75002cc641b5 mongo:3.6.3 "docker-entrypoint.s…" 12 seconds ago Up 22 seconds 0.0.0.0:27018->27017/tcp mongo-docker
$ docker exec -it mongo-docker mongo --version
MongoDB shell version v3.6.3
...
ポートは「27018」を指定しています。
Expressのサンプルアプリケーションの開発
フルスタックの経験が少ないエンジニアがSPAのアプリケーション開発しようとする場合、なぜかフロントエンドから作ろうとしがちですが、SPAのアプリケーションを作る場合はAPIから開発するのが基本です。(もちろん最低でもモックなどの見た目はあった方がよいですが)
Expressのプロジェクト作成
それでは、まずは必要なフォルダ作成やパッケージインストールからはじめましょう。
$ mkdir full-stack-app
$ cd full-stack-app/
$ brew install yarn
$ yarn global add nodemon
$ yarn init -y
$ yarn add express mongoose body-parser
$ mkdir server
$ touch server/server.js
$ mkdir server/models
$ touch server/models/User.js
$ tree server/
server/
├── models
│ └── User.js
└── server.js
MongooseでUserモデルを作る
MongoDBのスキーマを定義します。「User.js」は以下のようにします。
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
name: {
type: String,
required: true,
trim: true,
unique: 1
},
age: Number
});
const User = mongoose.model('users', userSchema);
module.exports = User;
ExpressでREST APIを作る
ここは、単純にUserオブジェクトをCRUDするREST APIを作りましょう。「server.js」は以下のようにしましょう。
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const PORT = process.env.PORT || 3001;
const User = require('./models/User');
mongoose.connect('mongodb://localhost:27018/sampledb');
const app = express();
app.use(bodyParser.json());
app.post('/users', (req, res) => {
const user = new User(req.body);
user.save((err, addedUser) => {
if (err)
return res.status(400).json({
errorMessage: 'faild to add the user. User name must be unique.'
});
res.send(addedUser);
});
});
app.get('/users', (req, res) => {
User.find({}, (err, users) => {
res.send(users);
});
});
app.get('/users/:id', (req, res) => {
const id = req.params.id;
User.findById(id, (err, user) => {
if (err)
return res.status(400).json({
errorMessage: 'the user not found.'
});
res.send(user);
});
});
app.delete('/users/:id', (req, res) => {
const id = req.params.id;
User.findByIdAndRemove(id, (err, deletedUser) => {
if (err)
return res.status(400).json({
errorMessage: 'faild to delete the user.'
});
res.send(deletedUser);
});
});
app.delete('/users', (req, res) => {
User.remove({}, err => {
if (err)
return res.status(400).json({
errorMessage: 'faild to delete all users.'
});
res.send(true);
});
});
app.listen(PORT, () => {
console.log(`Server up on ${PORT}`);
});
簡単ではありますがバックエンド完成です。
Expressのサンプルアプリケーションの動作確認
出来上がったバックエンドの動作確認をしましょう。コンソールを2つ開いて下さい。
1つ目のコンソールでExpressのサンプルアプリケーションを起動します。
$ node server/server.js
Server up on 3001
2つ目のコンソールからcurlで確認していきます。
$ curl -H 'Content-Type:application/json' -d '{"name":"Tom","age":20}' http://localhost:3001/users
{"name":"Tom","age":20,"_id":"5aa1486997f7dd76ae3ddec1","__v":0}
$ curl -H 'Content-Type:application/json' -d '{"name":"John","age":30}' http://localhost:3001/users
{"name":"John","age":30,"_id":"5aa1487397f7dd76ae3ddec2","__v":0}
$ curl -H 'Content-Type:application/json' -d '{"name":"Mary","age":25}' http://localhost:3001/users
{"name":"Mary","age":25,"_id":"5aa1487b97f7dd76ae3ddec3","__v":0}
$ curl http://localhost:3001/users | jq
...
[
{
"_id": "5aa1486997f7dd76ae3ddec1",
"name": "Tom",
"age": 20,
"__v": 0
},
{
"_id": "5aa1487397f7dd76ae3ddec2",
"name": "John",
"age": 30,
"__v": 0
},
{
"_id": "5aa1487b97f7dd76ae3ddec3",
"name": "Mary",
"age": 25,
"__v": 0
}
]
$ curl http://localhost:3001/users/5aa1486997f7dd76ae3ddec1 | jq
...
{
"_id": "5aa1486997f7dd76ae3ddec1",
"name": "Tom",
"age": 20,
"__v": 0
}
$ curl http://localhost:3001/users/5aa1487397f7dd76ae3ddec2 | jq
...
{
"_id": "5aa1487397f7dd76ae3ddec2",
"name": "John",
"age": 30,
"__v": 0
}
$ curl http://localhost:3001/users/5aa1487b97f7dd76ae3ddec3 | jq
...
{
"_id": "5aa1487b97f7dd76ae3ddec3",
"name": "Mary",
"age": 25,
"__v": 0
}
$ curl -X DELETE http://localhost:3001/users/5aa1486997f7dd76ae3ddec1
{"_id":"5aa1486997f7dd76ae3ddec1","name":"Tom","age":20,"__v":0}
$ curl http://localhost:3001/users | jq
...
[
{
"_id": "5aa1487397f7dd76ae3ddec2",
"name": "John",
"age": 30,
"__v": 0
},
{
"_id": "5aa1487b97f7dd76ae3ddec3",
"name": "Mary",
"age": 25,
"__v": 0
}
]
$ curl -X DELETE http://localhost:3001/users
true
$ curl http://localhost:3001/users | jq
...
[]
ちゃんと動いていますね。
Reactの開発準備
フロントエンドはひとまず雛形だけ作ります。
プロジェクトの作成
流行りの「create-react-app」を使ってささっと作りましょう。
$ yarn global add create-react-app
$ create-react-app client
$ cd client
$ yarn add axios
$ rm src/*
$ touch src/index.js
$ touch src/App.js
$ tree src/
src/
├── App.js
└── index.js
index.jsの作成
ここは一般的な形にします。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.jsの作成
ここも表示だけの最小限の形にします。
import React, { Component } from 'react';
class App extends Component {
render() {
return <div>Hello App!</div>;
}
}
export default App;
余談ですが、ここでエディタのVisual Studio Codeを使っていると、Reactのスニペットで一瞬でここまでできあがります。使っていない方はVSCodeのプラグインを探してみて下さい。
開発のためにフロントエンドとバックエンドを同時に起動させる
ここでフロントエンドとバックエンドをつなげて開発するための設定を行います。そうしないとコンソールをフロントエンドとバックエンドで2つも開かなければなりません。めんどくさいです。なので、同時起動の方法を説明します。YarnとNPMの両方の設定を載せますが、このチュートリアルではYarnを使っているので、最後までやりたい方はYarnの設定だけして下さい。
プロキシ設定
フロントエンドをポート3000、バックエンドをポート3001で起動して接続させるために、フロントエンドの「package.json」に設定をする必要があります。
$ cd client
フロントエンドの「package.json」に以下のスクリプトを追加します。「package.json」がフロントエンドとバックエンドで2つあるので注意して下さい。
"proxy": {
"/users/*": {
"target": "http://localhost:3001"
}
}
同時に起動させる方法(Yarn編)
プロジェクトのフォルダに移動します。
$ cd ..
$ pwd
/Users/you/full-stack-app
必要なパッケージをインストールします。
$ yarn add --dev npm-run-all react-scripts
バックエンドの「package.json」に以下のスクリプトを追加します。
"scripts": {
"start": "node server/server.js",
"dev:server": "nodemon server/server.js",
"dev:client": "yarn --cwd client start",
"dev": "npm-run-all -p dev:*"
},
あとは以下のコマンドで同時開発を開始できます。
$ yarn dev
同時に起動させる方法(NPM編)
ここまでYarnを使って開発していましたが、NPMでも似たような環境が作れます。
プロジェクトのフォルダに移動します。
$ cd ..
$ pwd
/Users/you/full-stack-app
必要なパッケージをインストールします。
$ npm install --save-dev concurrently
バックエンドの「package.json」に以下のスクリプトを追加します。
"scripts": {
"start": "node index.js",
"server": "nodemon index.js",
"client": "npm run start --prefix client",
"dev": "concurrently \"npm run server\" \"npm run client\""
},
同じく以下のコマンドで同時開発を開始できます。
$ npm run dev
起動すると以下の画面がブラウザで自動で立ち上がります。
これで、フロントエンドとバックエンドを同時に開発する環境ができあがりました。
Reactのサンプルアプリケーションの開発
ここまできたら、Reactを仕上げてしまいましょう。
今回はユーザ情報を管理する簡単なアプリケーションとして作っていきましょう。
デザイン用のReactコンポーネントのインストール
Reactの良いところは、さまざまなフロントエンド用のデザインがReactコンポーネントとして提供されていることです。今回はReact Materializeを使うことにします。
$ cd client
$ yarn add react-materialize
$ cd ..
$ pwd
/Users/you/full-stack-app
$ tree client/public/
client/public/
├── favicon.ico
├── index.html
└── manifest.json
$ tree client/src/
client/src/
├── App.js
└── index.js
$ yarn dev
最後のコマンドでReactとExpressが両方起動して表示されました。
index.htmlの作成
「index.html」は以下のようにします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!-- Import Google Icon Font -->
<link href="http://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Import materialize.css -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/css/materialize.min.css" rel="stylesheet">
<title>User Management App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!-- Import jQuery before materialize.js -->
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.0/js/materialize.min.js"></script>
</body>
</html>
単にReact Materialize用の設定を追加しただけです。
App.jsの作成
これはReactのメインですね。以下のようにします。
import React, { Component } from 'react';
import axios from 'axios';
import { Button, Row, Input } from 'react-materialize';
class App extends Component {
state = {
name: '',
age: '',
users: [],
error: '',
message: ''
};
componentWillMount() {
axios.get(`/users`).then(response => {
this.setState({ users: response.data });
});
}
handleInputName = event => {
this.setState({ name: event.target.value });
};
handleInputAge = event => {
this.setState({ age: event.target.value });
};
submitForm = event => {
event.preventDefault();
axios
.post(`/users`, {
name: this.state.name,
age: this.state.age
})
.then(response => {
this.setState({
users: [...this.state.users, response.data],
error: '',
message: `Info: ${response.data.name} was added.`
});
})
.catch(error => {
this.setState({
error: `Error: ${error.response.data.errorMessage}`,
message: ''
});
});
};
deleteUser = id => {
axios
.delete(`/users/${id}`)
.then(response => {
this.setState({
users: this.state.users.filter(
user => user._id !== response.data._id
),
error: '',
message: `Info: ${response.data.name} was deleted.`
});
})
.catch(error => {
this.setState({ error: `Error: ${error.response.data.errorMessage}` });
});
};
deleteAllUsers = event => {
axios
.delete(`/users`)
.then(response => {
this.setState({
users: [],
error: '',
message: 'Info: all users were deleted.'
});
})
.catch(error => {
this.setState({
error: `Error: ${error.response.data.errorMessage}`,
message: ''
});
});
};
clearMessage = event => {
this.setState({ message: '' });
};
clearError = event => {
this.setState({ error: '' });
};
renderUsers() {
const users = this.state.users;
return users.map(user => (
<tr key={user._id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>
<Button onClick={user_id => this.deleteUser(user._id)}>Delete</Button>
</td>
</tr>
));
}
render() {
return (
<div>
<div
style={
this.state.error
? {
backgroundColor: '#f72e2e',
textAlign: 'center',
padding: '10px 0'
}
: null
}
onClick={this.clearError}
>
{this.state.error}
</div>
<div
style={
this.state.message
? {
backgroundColor: '#42a7f4',
textAlign: 'center',
padding: '10px 0'
}
: null
}
onClick={this.clearMessage}
>
{this.state.message}
</div>
<div style={{ width: '80%', margin: '0 auto', textAlign: 'center' }}>
<h2>User Management App</h2>
<h4>Add New User</h4>
<form onSubmit={this.submitForm}>
<Row>
<Input
type="text"
placeholder="Enter name"
s={5}
label="Name"
value={this.state.name}
onChange={this.handleInputName}
/>
<Input
type="number"
placeholder="Enter age"
s={5}
label="Age"
value={this.state.age}
onChange={this.handleInputAge}
/>
<Button type="submit" s={2}>
Add
</Button>
</Row>
</form>
<h4>Delete All Users</h4>
<div>
<Button onClick={this.deleteAllUsers}>Delete All</Button>
</div>
<div>
<h4>Users</h4>
<table>
<thead>
<tr>
<th style={{ width: '30%' }}>Name</th>
<th style={{ width: '30%' }}>Age</th>
<th style={{ width: '30%' }} />
</tr>
</thead>
<tbody>{this.renderUsers()}</tbody>
</table>
</div>
</div>
</div>
);
}
}
export default App;
これでフロントエンドの開発も完了です!
完成したサンプルアプリケーションで遊んでみる
それでは最後に、作ったサンプルアプリケーションで遊んでみましょう。
ユーザを追加してみる
「yarn dev」で表示して確認しましょう。
名前と年齢を入力してADDボタンでユーザが追加されました。
同じ名前でADDボタンを押すとエラーが表示されました。
データベース上にもちゃんとデータが登録されています。
ユーザを削除してみる
ユーザの横のDELETEボタンを押すと、ユーザが削除されました。
データベース上からも削除させています。
DELETE ALLボタンを押すとユーザ情報が全て削除されました。
データベースも空です。
少し動きで気になる点がありますが、サンプルアプリケーションはうまく動いているようです。せっかくなので、何か機能を追加してみて下さい。
最後に
いかがでしたか?これでReactとNodeJSでフロントエンドとバックエンドを同時に開発することができるようになったのではないでしょうか。それにしてもJavascriptでフロントエンドもバックエンドも開発できるのは本当に素晴らしいです。小さい規模の開発であればバックエンドでNodeJS以外を選択する理由はほとんどありませんし、フロントエンドはReactを選んでおけば今のところ間違いありません。それでは。
環境
- PC : macOS High Sierra 10.13.3
- docker : Docker version 17.12.0-ce, build c97c6d6
- mongoイメージ : 3.6.3
- NodeJS : v9.5.0
- yarn : 1.3.2
- create-react-app : 1.5.2
- [server] express : 4.16.2
- [server] mongoose : 5.0.7
- [server] body-parser : 1.18.2
- [server] npm-run-all : 4.1.2
- [server] react-scripts : 1.1.1
- [server] concurrently : 3.5.1
- [client] axios : 0.18.0
- [client] react : 16.2.0
- [client] react-dom : 16.2.0
- [client] react-scripts : 1.1.1
- [client] react-materialize : 2.1.0


コメントを残す