【サーバーレスで構築】kintoneとLINE連携!Flex Messageを活用した商品カタログBotの実装手順(AWS Lambda/TypeScript)
この記事は「BEMA Lab Advent Calendar 2025」の4日目の記事です。
※本アドベントカレンダーの4日目の投稿となります。
はじめに
本記事では、kintone・AWS
・LINE
を組み合わせて、商品カタログをLINE上で閲覧できるアプリを紹介します。管理者はkintoneで商品データを登録し、利用者(お客様)はLINEから商品一覧を確認します。構成としてはLINE Botに少し機能を足しただけのシンプルなものですが、LINEという使い慣れたUIで操作できます。身近なサービスを連携させて、実用的なアプリを素早く構築する例として紹介します。また、サーバーレス構成でメンテナンスコストを削減、フロントエンドの開発を最小限に抑えることもできます。

システム構成図
お客様がLINEでメッセージを送信すると、LINEサーバーから送信されるWebhookをLambdaで処理します。
Lambdaではkintoneから商品データを取得し、LINEのFlex Messageを用いて商品カタログを表示します。

前提条件
- Node.js ver.22
- LINE Bot SDK ver.10.5.0
- Express ver.5.1.0
- TypeScript ver.5.9.3
本記事で利用するサービスは基本的には無料で利用できます。kintoneは検証目的で開発者ライセンスを利用します。
kintoneでつくる管理者側画面
kintoneはサイボウズ社が提供しているローコードツールです。
UI部品をドラッグ・アンド・ドロップして画面を作成して、データを保存できます。ITエンジニア以外でも業務システムを作成できます。
商品データの登録を想定して、以下のような項目を持つ画面を作成します。

項目 | UI部品種別 | フィールドコード |
タイトル | 1行テキストボックス | title |
商品説明 | 複数行テキストボックス | description |
価格 | 数値 | price |
販売開始日 | 日付 | sellStartDate |
納品日数 | 数値 | leadTime |
フィールドコードは画面内で項目を一意に特定できる名前です。APIリクエストで指定したり、レスポンスに含まれます。
LINEでつくるお客様側画面
お客様はLINEで商品カタログを閲覧できるようにします。
LINEでメッセージを送信すると、サーバー側でkintoneから商品データを取得します。
商品データを基に、画像や段組レイアウトを組み合わせて見やすいレイアウトでLINE上にカタログを表示します。
AWS LambdaでのWebhook処理
利用者がLINEで送信されたメッセージは、LINEサーバーからWebhookで受信して処理します。
今回はNode.js環境のTypeScriptでBotサーバーを作成します。
必要なライブラリをインストール
Webhookを受信するサーバーはExpressを用いて処理します。Lambdaで動作させるため**@codegenie/serverless-express**を利用します。
LINEの送受信は、公式から提供されているSDK(Software Development Kit)**@line/bot-sdk**を用いると便利です。
$ yarn add @line/bot-sdk express @codegenie/serverless-express
$ yarn add -D typescript @types/express @types/aws-lambdaBotサーバーへはどこからでもアクセスされる可能性があるため、LINEの開発ガイドラインに沿って正規のLINEサーバーから送信されたことを検証します。
SDKを使うと簡単に検証できます。
// src/index.ts
import express, { Application } from "express";
import serverlessExpress from "@codegenie/serverless-express";
import { middleware } from "@line/bot-sdk";
import "dotenv/config";
import router from "./router";
const app: Application = express();
const lineConfig = {
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN!,
channelSecret: process.env.LINE_CHANNEL_SECRET!,
};
app.use("/line", middleware(lineConfig)); // LINE署名検証
app.use(router);
export const handler = serverlessExpress({ app });以下のファイルも作成しておきます。
// src/router.ts
import express from "express";
import LineController from "./controller.js";
const router = express.Router();
router.post("/line/webhook", async (req, res) => {
const controller = new LineController();
await controller.webhook(req);
res.sendStatus(200);
});
export default router;ここで、AWS Lambdaの新しい関数を作成し、ソースコードをアップロードします。Webhookで呼び出せるように、関数URLも作成しておきます。
また、Lambdaの環境変数として以下の値を設定しておきます。
環境変数 | 設定値 |
LINE_CHANNEL_ACCESS_TOKEN | LINEチャネルアクセストークン |
LINE_CHANNEL_SECRET | LINEチャネルシークレット |
KINTONE_BASE_URL | kintone アプリのURL( 例: https://example.cybozu.com) |
KINTONE_API_TOKEN | kintoneAPIトークン |
KINTONE_APP_ID | アプリID |
ここまででLINEサーバーからWebhookを受信する処理ができました。
LINE Console上で「Webhook URL」に以下のURLを設定して検証ボタンを押下し、通信が成功すれば問題ありません。
https://【AWS Lambdaの関数URL】/line/webhook
kintoneからの商品データ取得
商品データはkintoneからAPIを用いて取得します。
kintoneでは公式APIを用いることで、データを取得したり、更新したりできます。
今回は条件に合致する複数の商品レコードを取得したいので「複数のレコードを取得する」のAPIを利用します。
エンドポイントは以下のようになります。
https://【サブドメイン】.cybozu.com/k/v1/records.json認証方式はいくつかありますが、今回は「APIトークン認証」を利用します。
これはアプリおよび権限(レコード読み取り、更新など)ごとに発行できる形式です。発行手順は公式ドキュメントを参照してください。
APIリクエスト時は次のようにヘッダーに設定します。
const res = await fetch(url.href, {
headers: {
"X-Cybozu-API-Token": process.env.KINTONE_API_TOKEN!, // APIトークンを渡す
},
});APIリクエスト時はクエリパラメータで検索条件を指定できます。
今回は販売中の商品を取得したいので「販売開始日 <= システム日付 」を条件(query)に指定します。
また、新商品に注目させたいので、販売開始日の順で並び替えます。
const url = new URL(`${process.env.KINTONE_BASE_URL!}/k/v1/records.json`);
url.searchParams.append("app", process.env.KINTONE_APP_ID!);
url.searchParams.append(
"query",
sellStartDatetime <= TODAY() order by sellStartDatetime desc, // 取得条件
);APIレスポンスは何も指定しないとany型になってしまいます。
kintone-dts-genというツールを利用すると、kintoneアプリの項目名、UIパーツに応じてTypeScriptの型定義ファイル(d.ts)を生成できます。
詳細な手順は公式ドキュメントを参照してください。
出力結果として、次のような型定義ファイルが生成されます。
// types/kintone-ecapp.d.ts
declare namespace kintoneTypes {
interface ProductCatalogAppFields {
description: kintone.fieldTypes.MultiLineText;
leadTime: kintone.fieldTypes.Number;
title: kintone.fieldTypes.SingleLineText;
price: kintone.fieldTypes.Number;
sellStartDate: kintone.fieldTypes.Date;
}
interface SavedProductCatalogAppFields extends ProductCatalogAppFields {
$id: kintone.fieldTypes.Id;
// 中略
}
}生成したファイルと、kintone自体の型定義ファイルをtsconfig.jsonに追記しておきます。
// tsconfig.json
"files": [
"./node_modules/@kintone/dts-gen/kintone.d.ts",
"./types/kintone-ecapp.d.ts",
],recordsの型としてSavedLINEECAppFieldsを指定します。
/** kintone APIのレスポンス */
type KintoneECAppResponse = {
records: kintoneTypes.SavedProductCatalogAppFields[];
};
const itemDetails: KintoneECAppResponse = await res.json();コード全体は次のようになります。
// src/KintoneService.ts
export class KintoneService {
public async getECProducts(): Promise<Product[]> {
const url = new URL(`${process.env.KINTONE_BASE_URL!}/k/v1/records.json`);
url.searchParams.append("app", process.env.KINTONE_APP_ID!);
url.searchParams.append(
"query",
sellStartDate <= TODAY() order by sellStartDate desc
);
const res = await fetch(url.href, {
headers: {
"X-Cybozu-API-Token": process.env.KINTONE_API_TOKEN!,
},
});
if (!res.ok) {
throw new Error("kintone API実行(商品取得)に失敗しました");
}
const productData: KintoneProductCatalogAppResponse = await res.json();
return productData.records.map((item) => ({
title: item.title.value,
description: item.description.value,
price: Number(item.price.value),
leadTime: Number(item.leadTime.value),
}));
}
}
/** 商品 */
export interface Product {
/** タイトル */
title: string;
/** 説明 */
description: string;
/** 価格 */
price: number;
/** 納品日数 */
leadTime: number;
}
/** kintone APIのレスポンス */
type KintoneProductCatalogAppResponse = {
records: kintoneTypes.SavedProductCatalogAppFields[];
};Flex Messgeを用いた商品カタログ表示
kintoneから取得した商品データを用いて、LINEで商品カタログを表示します。LINEではFlex Messageを使うと、テキストだけではなく、画像と組み合わせてレイアウトしたり、複数のメッセージをスワイプして表示したりできます。柔軟なレイアウトを作成できるため、ユーザーへの視覚的な訴求効果が高くなります。
今回は次のようなレイアウトを実装してみます。
- 水平方向に商品を並べスワイプして閲覧
- 画像とテキストを1つのメッセージで組み合わせて表示
- フォントサイズや色を変える

Flex MessageはJSONで定義します。
上から順にコンテナ、ブロック、コンポーネントの3層構造となっています。
message = {
type: "flex",
altText: "商品カタログ",
contents: {
type: "carousel", // 1. コンテナ(カルーセル)
contents: ecData.map((item) => ({
type: "bubble",
hero: { // 2. ブロック(hero block)
type: "image", // 3. コンポーネント(画像)
url: "【背景画像URL】",
size: "full",
aspectRatio: "20:13",
aspectMode: "cover",
},
body: { // 2. ブロック(body block)
type: "box",
layout: "vertical",
contents: [
{
type: "text", // 3. コンポーネント(テキスト)
text: item.title,
weight: "bold",
size: "xl",
},
// 中略
],
},
})),
},
};1. コンテナ
コンテナはメッセージ表示数を定義します。今回は複数表示したいので「カルーセル」を指定します。
2. ブロック
ブロックは1つのメッセージのレイアウトを定義します。
今回は、上から順にhero blockに画像、body blockに商品説明などのテキストを配置します。
3. コンポーネント
コンポーネントは各ブロックに表示するUI部品を定義します。hero blockには画像を単体で表示します。body blockは「画像」と「テキスト」「ボックス」を表示します。
TypeScriptでは型定義の恩恵を受けられるとはいえ、なにもない状態からJSONでFlex Messageを作成するのは難しい場合があります。
公式で提供されているFlex Message Simulatorを使うことで、GUIでレイアウトを作成できます。JSONも取得できるので便利です。
ローディング表示
ユーザーがメッセージを送信してから、商品カタログを表示するまでに時間を要する場合があります。
何も応答がないと、何度も送信したり、離脱してしまったりとユーザーの体験が悪くなってしまいます。
これを防ぐため、Webhookでメッセージを受信したらLINEでローディング表示するようにします。
SDKを用いると次のコードで表示できます。ローディングの最大表示秒数をloadingSecondsを指定します。時間が経過しなくても、Botサーバーからメッセージを返信しだい表示は消えます。
// ローディング表示
await this.client.showLoadingAnimation({
chatId: 【LINE ID】,
loadingSeconds: 30, // 最大表示秒数
});コード全体は次のようになります。
// src/LINEService.ts
import { messagingApi } from "@line/bot-sdk";
import { WebhookEvent } from "@line/bot-sdk";
import { Product, KintoneService } from "./KintoneService.js";
const { MessagingApiClient } = messagingApi;
/**
* LINEサービス
*/
export class LineService {
/** LINEクライアント */
private client: messagingApi.MessagingApiClient;
constructor() {
this.client = new MessagingApiClient({
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN!,
});
}
/**
* LINEメッセージの処理
* @param events LINE Webhookイベント
* @returns
*/
public async processWebhook(events: WebhookEvent[]): Promise<void> {
try {
for (let i = 0; i < events.length; i++) {
const event = events[i] as WebhookEvent;
if (!event || (event.type !== "message" && event.type !== "postback")) {
// LINE Consoleでの動作検証用
return;
}
let message = ""; // ユーザーが送信したメッセージ
if ("message" in event && event.message.type === "text") {
message = event.message.text;
}
const lineId = event.source.userId; // ユーザーのLINE ID(返信に必要)
if (!lineId) {
throw new Error("LINE IDを取得できませんでした");
}
// ローディング表示
await this.client.showLoadingAnimation({
chatId: lineId,
loadingSeconds: 10,
});
// kintoneからEC商品取得
const kintoneService = new KintoneService();
const ecData = await kintoneService.getECProducts();
// LINEでカタログをFlex Messageで送信
await this.showECCatalog(event.replyToken, ecData);
}
} catch (error) {
console.error(error);
throw error;
}
}
/**
* 商品カタログをFlex Messageで表示する
* @param replyToken LINEリプライトークン
* @param ecData 商品データ
*/
private async showECCatalog(
replyToken: string,
ecData: Product[]
): Promise<void> {
let message: messagingApi.Message = {
type: "text",
text: "商品がありません",
};
if (ecData.length > 0) {
message = {
type: "flex",
altText: "商品カタログ",
contents: {
type: "carousel",
contents: ecData.map((item) => ({
type: "bubble",
hero: {
type: "image",
url: "【背景画像URL】",
size: "full",
aspectRatio: "20:13",
aspectMode: "cover",
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: item.title,
weight: "bold",
size: "xl",
wrap: true,
},
{
type: "text",
text: item.description,
size: "sm",
wrap: true,
},
{
type: "box",
layout: "vertical",
margin: "lg",
spacing: "sm",
contents: [
{
type: "box",
layout: "baseline",
spacing: "sm",
contents: [
{
type: "icon",
url: "【価格アイコンURL】",
offsetTop: "xs",
},
{
type: "text",
text: ${item.price}円,
wrap: true,
color: "#666666",
size: "sm",
flex: 5,
margin: "sm",
offsetStart: "sm",
},
],
alignItems: "center",
},
{
type: "box",
layout: "baseline",
spacing: "sm",
contents: [
{
type: "icon",
url: "【納品日数アイコンURL】",
offsetTop: "xs",
},
{
type: "text",
text: ${item.leadTime}日後にお届け,
wrap: true,
color: "#666666",
size: "sm",
flex: 5,
margin: "sm",
offsetStart: "sm",
},
],
alignItems: "center",
},
],
},
],
},
})),
},
};
}
try {
await this.client.replyMessage({
replyToken: replyToken,
messages: [message],
});
} catch (error) {
console.error(error);
throw error;
}
}
}
まとめ
この記事では、kintoneとLINEをAWSのサーバーレスな構成でつなぎ合わせて、商品カタログBotを構築できることを説明しました。
kintoneでの柔軟なアプリ開発とデータ利用、LINEの使い慣れたUIを用いることで複雑なフロントエンド開発しなくてもBotを構築できます。
この記事を書いた人

関連記事


バッチ処理の設計・実装で失敗しないための鉄則8選Hideki Ikemoto

What is BEMA!?
Be Engineer, More Agile


