ノードを使用して軽量の請求アプリを構築する方法:データベースとAPI
序章
請求書は、企業が顧客やクライアントに提示できる商品やサービスの文書です。
デジタル請求ツールは、クライアントを追跡し、サービスと価格を記録し、支払い済みの請求書のステータスを更新し、請求書を表示するためのインターフェイスを提供する必要があります。 これには、CRUD(作成、読み取り、更新、削除)、データベース、およびルーティングが必要になります。
注:これは3部構成のシリーズのパート1です。 2番目のチュートリアルは、ノードを使用して軽量の請求書作成アプリを構築する方法:ユーザーインターフェイスです。 3番目のチュートリアルは、 Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信です。
このチュートリアルでは、VueとNodeJSを使用して請求書発行アプリケーションを構築します。 このアプリケーションは、請求書の作成、送信、編集、削除などの機能を実行します。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
- SQLiteはローカルにインストールされます。これは、SQLiteのインストールと使用方法に従って実行できます。
- APIエンドポイントをテストするには、Postmanなどのツールをダウンロードしてインストールする必要があります。
注: SQLiteは現在、デフォルトでmacOSおよびMacOSXにプリインストールされています。
このチュートリアルは、Node v16.1.0、npm
v7.12.1、およびSQLitev3.32.3で検証されました。
ステップ1—プロジェクトの設定
要件がすべて設定されたので、次に行うことは、アプリケーションのバックエンドサーバーを作成することです。 バックエンドサーバーはデータベース接続を維持します。
新しいプロジェクトのディレクトリを作成することから始めます。
- mkdir invoicing-app
新しく作成されたプロジェクトディレクトリに移動します。
- cd invoicing-app
次に、ノードプロジェクトとして初期化します。
- npm init -y
サーバーが適切に機能するためには、インストールする必要のあるノードパッケージがいくつかあります。 次のコマンドを実行して、それらをインストールできます。
- npm install bcrypt@5.0.1 bluebird@3.7.2 cors@2.8.5 express@4.17.1 lodash@4.17.21 multer@1.4.2 sqlite3@5.0.2^> umzug@2.3.0<^>
このコマンドは、次のパッケージをインストールします。
bcrypt
ユーザーパスワードをハッシュするbluebird
移行を作成するときにPromiseを使用するcors
クロスオリジンリソースシェアリング用express
は、Webアプリケーションを強化します- ユーティリティメソッドの場合は
lodash
multer
は着信フォームリクエストを処理しますsqlite3
は、データベースを作成および保守しますumzug
データベース移行を実行するタスクランナーとして
注:最初の発行以降、このチュートリアルはisEmpty()
のlodash
を含むように更新されました。 multipart/form-data
を処理するミドルウェアライブラリがconnect-multiparty
からmulter
に変更されました。
アプリケーションロジックを格納するserver.js
ファイルを作成します。 server.js
ファイルで、必要なモジュールをインポートし、Expressアプリを作成します。
const express = require('express');
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const PORT = process.env.PORT || 3128;
const app = express();
app.use(express.urlencoded({extended: false}));
app.use(express.json());
app.use(cors());
// ...
/
ルートを作成して、サーバーが機能することをテストします。
// ...
app.get('/', function(req, res) {
res.send('Welcome to Invoicing App.');
});
app.listen()
は、着信ルートをリッスンするポートをサーバーに指示します。
// ...
app.listen(PORT, function() {
console.log(`App running on localhost:${PORT}.`);
});
サーバーを起動するには、プロジェクトディレクトリで次を実行します。
- node server
これで、アプリケーションは着信要求のリッスンを開始します。
ステップ2—SQLiteを使用したデータベースの作成と接続
請求書発行アプリケーションの場合、既存の請求書を保存するためのデータベースが必要です。 SQLiteは、このアプリケーションに最適なデータベースクライアントになります。
database
フォルダーを作成することから始めます。
- mkdir database
sqlite3
クライアントを実行し、次の新しいディレクトリにデータベース用のInvoicingApp.db
ファイルを作成します。
- sqlite3 database/InvoicingApp.db
データベースが選択されたので、次は必要なテーブルを作成します。
このアプリケーションは、次の3つのテーブルを使用します。
- 「ユーザー」-これにはユーザーデータが含まれます(
id
、name
、email
、company_name
、password
) - 「請求書」-請求書のデータを保存します(
id
、name
、paid
、user_id
) - 「トランザクション」-請求書を作成するために一緒になる単一のトランザクション(
name
、price
、invoice_id
)
必要なテーブルが特定されたので、次のステップはテーブルを作成するためのクエリを実行することです。
移行は、アプリケーションの成長に合わせてデータベースの変更を追跡するために使用されます。 これを行うには、database
ディレクトリにmigrations
フォルダを作成します。
- mkdir database/migrations
これは、すべての移行ファイルの場所になります。
次に、migrations
フォルダーに1.0.js
ファイルを作成します。 この命名規則は、最新の変更を追跡するためのものです。
1.0.js
ファイルで、最初にノードモジュールをインポートします。
"use strict";
const path = require('path');
const Promise = require('bluebird');
const sqlite3 = require('sqlite3');
// ...
次に、移行ファイルの実行時に実行されるup
関数と、データベースへの変更を元に戻すdown
関数をエクスポートします。
// ...
module.exports = {
up: function() {
return new Promise(function(resolve, reject) {
let db = new sqlite3.Database('./database/InvoicingApp.db');
db.run(`PRAGMA foreign_keys = ON`);
// ...
up
関数では、最初にデータベースへの接続が行われます。 次に、sqlite
データベースで外部キーが有効になります。 SQLiteでは、下位互換性を確保するために外部キーはデフォルトで無効になっているため、すべての接続で外部キーを有効にする必要があります。
次に、テーブルを作成するためのクエリを指定します。
// ...
db.serialize(function() {
db.run(`CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT,
company_name TEXT,
password TEXT
)`);
db.run(`CREATE TABLE invoices (
id INTEGER PRIMARY KEY,
name TEXT,
user_id INTEGER,
paid NUMERIC,
FOREIGN KEY(user_id) REFERENCES users(id)
)`);
db.run(`CREATE TABLE transactions (
id INTEGER PRIMARY KEY,
name TEXT,
price INTEGER,
invoice_id INTEGER,
FOREIGN KEY(invoice_id) REFERENCES invoices(id)
)`);
});
db.close();
});
}
}
serialize()
関数は、クエリが同時にではなく順次実行されるように指定するために使用されます。
移行ファイルが作成されたら、次のステップはそれらを実行してデータベースに変更を加えることです。 これを行うには、アプリケーションのルートからscripts
フォルダーを作成します。
- mkdir scripts
次に、この新しいディレクトリにmigrate.js
というファイルを作成します。 そして、migrate.js
ファイルに以下を追加します。
const path = require('path');
const Umzug = require('umzug');
let umzug = new Umzug({
logging: function() {
console.log.apply(null, arguments);
},
migrations: {
path: './database/migrations',
pattern: /\.js$/
},
upName: 'up'
});
// ...
まず、必要なノードモジュールがインポートされます。 次に、新しいumzug
オブジェクトが構成とともに作成されます。 移行スクリプトのpath
およびpattern
も指定されています。 構成の詳細については、 umzugREADMEを参照してください。
また、詳細なフィードバックを提供するには、以下に示すようにイベントをログに記録する関数を作成し、最後にup
関数を実行して、移行フォルダーで指定されたデータベースクエリを実行します。
// ...
function logUmzugEvent(eventName) {
return function(name, migration) {
console.log(`${name} ${eventName}`);
};
}
// using event listeners to log events
umzug.on('migrating', logUmzugEvent('migrating'));
umzug.on('migrated', logUmzugEvent('migrated'));
umzug.on('reverting', logUmzugEvent('reverting'));
umzug.on('reverted', logUmzugEvent('reverted'));
// this will run your migrations
umzug.up().then(console.log('all migrations done'));
ここで、スクリプトを実行するには、ターミナルに移動し、アプリケーションのルートディレクトリで次のコマンドを実行します。
- node scripts/migrate.js
次のような出力が表示されます。
Outputall migrations done
== 1.0: migrating =======
1.0 migrating
この時点で、migrate.js
スクリプトを実行すると、1.0.js
構成がInvoicingApp.db
に適用されます。
ステップ3—アプリケーションルートの作成
データベースが適切に設定されたので、次はserver.js
ファイルに戻ってアプリケーションルートを作成します。 このアプリケーションでは、次のルートが利用可能になります。
URL | 方法 | 働き |
---|---|---|
/register |
POST |
新規ユーザーを登録するには |
/login |
POST |
既存のユーザーにログインするには |
/invoice |
POST |
新しい請求書を作成するには |
/invoice/user/{user_id} |
GET |
ユーザーのすべての請求書を取得するには |
/invoice/user/{user_id}/{invoice_id} |
GET |
特定の請求書を取得するには |
/invoice/send |
POST |
クライアントに請求書を送信するには |
POST /register
新しいユーザーを登録するために、サーバーの/register
ルートに対してPOSTリクエストが行われます。
server.js
に再度アクセスし、次のコード行を追加します。
// ...
const _ = require('lodash');
const multer = require('multer');
const upload = multer();
const bcrypt = require('bcrypt');
const saltRounds = 10;
// POST /register - begin
app.post('/register', upload.none(), function(req, res) {
// check to make sure none of the fields are empty
if (
_.isEmpty(req.body.name)
|| _.isEmpty(req.body.email)
|| _.isEmpty(req.body.company_name)
|| _.isEmpty(req.body.password)
) {
return res.json({
"status": false,
"message": "All fields are required."
});
}
// any other intended checks
// ...
空のフィールドがあるかどうか、および送信されるデータがすべての仕様に一致するかどうかを確認するためのチェックが行われます。 エラーが発生した場合、エラーメッセージが応答としてユーザーに送信されます。 そうでない場合は、パスワードがハッシュされ、データがデータベースに保存され、ユーザーに登録されたことを通知する応答が送信されます。
// ...
bcrypt.hash(req.body.password, saltRounds, function(err, hash) {
let db = new sqlite3.Database('./database/InvoicingApp.db');
let sql = `INSERT INTO
users(
name,
email,
company_name,
password
)
VALUES(
'${req.body.name}',
'${req.body.email}',
'${req.body.company_name}',
'${hash}'
)`;
db.run(sql, function(err) {
if (err) {
throw err;
} else {
return res.json({
"status": true,
"message": "User Created."
});
}
});
db.close();
});
});
// POST /register - end
ここで、Postmanなどのツールを使用してPOSTリクエストをname
、email
、company_name
、password
で/register
に送信するとします。 、新しいユーザーを作成します:
鍵 | 価値 |
---|---|
name |
テストユーザー |
email |
[email protected] |
company_name |
テスト会社 |
password |
パスワード |
クエリを使用してUsers
テーブルを表示し、ユーザーの作成を確認できます。
- select * from users;
これで、データベースに新しく作成されたユーザーが含まれます。
Output1|Test User|[email protected]|Test Company|[hashed password]
これで、/register
ルートが確認されました。
POST /login
既存のユーザーが/login
ルートを使用してシステムにログインしようとする場合、ユーザーは自分の電子メールアドレスとパスワードを提供する必要があります。 それを行うと、ルートは次のようにリクエストを処理します。
// ...
// POST /login - begin
app.post('/login', upload.none(), function(req, res) {
let db = new sqlite3.Database('./database/InvoicingApp.db');
let sql = `SELECT * from users where email='${req.body.email}'`;
db.all(sql, [], (err, rows) => {
if (err) {
throw err;
}
db.close();
if (rows.length == 0) {
return res.json({
"status": false,
"message": "Sorry, wrong email."
});
}
// ...
特定の電子メールを持つユーザーのレコードをフェッチするために、データベースに対してクエリが実行されます。 結果が空の配列を返す場合は、ユーザーが存在しないことを意味し、エラーをユーザーに通知する応答が送信されます。
データベースクエリがユーザーデータを返す場合、入力されたパスワードがデータベース内のそのパスワードと一致するかどうかを確認するために、さらにチェックが行われます。 含まれている場合は、ユーザーデータとともに応答が送信されます。
// ...
let user = rows[0];
let authenticated = bcrypt.compareSync(req.body.password, user.password);
delete user.password;
if (authenticated) {
return res.json({
"status": true,
"user": user
});
}
return res.json({
"status": false,
"message": "Wrong password. Please retry."
});
});
});
// POST /login - end
// ...
ルートがテストされると、成功または失敗した結果が表示されます。
これで、Postmanなどのツールを使用してemail
およびpassword
を使用して/login
にPOSTリクエストを送信すると、応答が返されます。
鍵 | 価値 |
---|---|
email |
[email protected] |
password |
パスワード |
このユーザーはデータベースに存在するため、次の応答が返されます。
Output{
"status": true,
"user": {
"id": 1,
"name": "Test User",
"email": "[email protected]",
"company_name": "Test Company"
}
}
これで、/login
ルートが確認されました。
POST /invoice
/invoice
ルートは、請求書の作成を処理します。 ルートに渡されるデータには、ユーザーID、請求書の名前、および請求書のステータスが含まれます。 また、請求書を構成するための単一のトランザクションも含まれます。
サーバーはリクエストを次のように処理します。
// ...
// POST /invoice - begin
app.post('/invoice', upload.none(), function(req, res) {
// validate data
if (_.isEmpty(req.body.name)) {
return res.json({
"status": false,
"message": "Invoice needs a name."
});
}
// perform other checks
// ...
最初に、サーバーに送信されたデータが検証されます。 次に、後続のクエリのためにデータベースへの接続が確立されます。
// ...
// create invoice
let db = new sqlite3.Database('./database/InvoicingApp.db');
let sql = `INSERT INTO invoices(
name,
user_id,
paid
)
VALUES(
'${req.body.name}',
'${req.body.user_id}',
0
)`;
// ...
請求書の作成に必要なINSERT
クエリが書き込まれ、実行されます。 その後、単一のトランザクションがtransactions
テーブルに挿入され、それらを参照するための外部キーとしてinvoice_id
が使用されます。
// ...
db.serialize(function() {
db.run(sql, function(err) {
if (err) {
throw err;
}
let invoice_id = this.lastID;
for (let i = 0; i < req.body.txn_names.length; i++) {
let query = `INSERT INTO
transactions(
name,
price,
invoice_id
) VALUES(
'${req.body.txn_names[i]}',
'${req.body.txn_prices[i]}',
'${invoice_id}'
)`;
db.run(query);
}
return res.json({
"status": true,
"message": "Invoice created."
});
});
});
});
// POST /invoice - end
// ...
ここで、Postmanなどのツールを使用してPOSTリクエストをname
、user_id
、txn_names
、txn_prices
で/invoice
に送信するとします。 、新しい請求書を作成し、トランザクションを記録します。
鍵 | 価値 |
---|---|
name |
テスト請求書 |
user_id |
1 |
txn_names |
iPhone |
txn_prices |
600 |
txt_names |
マックブック |
txn_prices |
1700 |
次に、請求書テーブルを確認します。
- select * from invoices;
次の結果を確認します。
Output1|Test Invoice|1|0
次のコマンドを実行します。
- select * from transactions;
次の結果を確認します。
Output1|iPhone|600|1
2|Macbook|1700|1
これで、/invoice
ルートが確認されました。
GET /invoice/user/{user_id}
これで、ユーザーが作成されたすべての請求書を表示したい場合、クライアントはGET
リクエストを/invoice/user/:id
ルートに送信します。 user_id
はルートパラメータとして渡されます。 リクエストは次のように処理されます。
// ...
// GET /invoice/user/:user_id - begin
app.get('/invoice/user/:user_id', upload.none(), function(req, res) {
let db = new sqlite3.Database('./database/InvoicingApp.db');
let sql = `SELECT * FROM invoices WHERE user_id='${req.params.user_id}' ORDER BY invoices.id`;
db.all(sql, [], (err, rows) => {
if (err) {
throw err;
}
return res.json({
"status": true,
"invoices": rows
});
});
});
// GET /invoice/user/:user_id - end
// ...
クエリが実行され、特定のユーザーに属するすべての請求書と請求書に関連するトランザクションがフェッチされます。
ユーザーのすべての請求書のリクエストについて考えてみます。
localhost:3128/invoice/user/1
次のデータで応答します。
Output{"status":true,"invoices":[{"id":1,"name":"Test Invoice","user_id":1,"paid":0}]}
これで、/invoice/user/:user_id
ルートが確認されました。
GET /invoice/user/{user_id}/{invoice_id}
特定の請求書を取得するには、user_id
およびinvoice_id
を使用して/invoice/user/{user_id}/{invoice_id}
ルートにGET
リクエストを送信します。 リクエストは次のように処理されます。
// ...
// GET /invoice/user/:user_id/:invoice_id - begin
app.get('/invoice/user/:user_id/:invoice_id', upload.none(), function(req, res) {
let db = new sqlite3.Database('./database/InvoicingApp.db');
let sql = `SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${req.params.user_id}' AND invoice_id='${req.params.invoice_id}' ORDER BY transactions.id`;
db.all(sql, [], (err, rows) => {
if (err) {
throw err;
}
return res.json({
"status": true,
"transactions": rows
});
});
});
// GET /invoice/user/:user_id/:invoice_id - end
// set application port
// ...
クエリを実行して、単一の請求書と、ユーザーに属する請求書に関連するトランザクションをフェッチします。
ユーザーの特定の請求書のリクエストについて考えてみます。
localhost:3128/invoice/user/1/1
次のデータで応答します。
Output{"status":true,"transactions":[{"id":1,"name":"iPhone","user_id":1,"paid":0,"price":600,"invoice_id":1},{"id":2,"name":"Macbook","user_id":1,"paid":0,"price":1700,"invoice_id":1}]}
これで、/invoice/user/:user_id/:invoice_id
ルートが確認されました。
結論
このチュートリアルでは、軽量の請求書発行アプリケーションに必要なすべてのルートを使用してサーバーをセットアップします。
ノードを使用して軽量の請求書作成アプリを構築する方法:ユーザーインターフェイスで学習を続けてください。