🏙️
Remix(+Graphql)をSSTでデプロイ
試したかったこと
- RemixをSSTでデプロイできるか?
- RemixのSSR部分をGraphqlサーバーにできないか?
結論
- RemixをSSTでデプロイできるか? → できる
- RemixのSSR部分をGraphqlサーバーとしても利用できないか? → できる
成果物↓
リポジトリ
技術スタック
- SST(serverless stack):pulumiをベースとしたIaC。今回はAWSにデプロイ。
- Remix:Reactをベースとしたフルスタックフレームワーク。
- pothos:コードファーストGraphqlサーバー。
- urql:GraphQLクライアント
前提条件
% asdf list
bun
*1.2.2
nodejs
*20.9.0
% aws --version
aws-cli/2.24.1 Python/3.12.9 Darwin/24.4.0 source/x86_64
手順
まずはSSTのベースを構築
こちらからSSTのモノレポの雛形を落としてきます。
SSTの公式にあるように、自分のプロジェクト名に変えます(MY_APPのところ)。
npx replace-in-file /monorepo-template/g MY_APP **/*.* --verbose
パッケージをインストールします
npm install
ローカルにAWS認証情報がある状態であれば、この状態でSSTのデプロイが可能です。
npx sst dev
こちらはLambdaでHelloWorldを返すだけのサンプルとなっています。
Remixを初期化
packages配下のcoreとfunctionsは不要なので削除します。
cd packages
rm -rf core
rm -rf functions
packages配下のwebにremixをインストールします。
# packages配下にwebフォルダ作成
mkdir web
cd web
# ※注意。モノレポなのでGithubリポジトリは「作らない」を選んでください。
npx create-remix@latest .
GraphQL関連パッケージ
必要なパッケージインストール
# Graphqlサーバー、クライアントインストール
npm i graphql-yoga @graphql-yoga/common @pothos/core
npm i urql
GraphQLスキーマ作成
Schema作成
従業員データのQueryと従業員追加Mutationを作っておきます。
// packages/web/app/graphql/schema.ts
import SchemaBuilder from "@pothos/core";
export interface Employee {
id: number;
name: string;
}
const builder = new SchemaBuilder<{
Objects: {
Employee: Employee;
};
}>({});
builder.objectType("Employee", {
fields: (t) => ({
id: t.exposeInt("id"),
name: t.exposeString("name"),
}),
});
const employees: Employee[] = [
{ id: 1, name: "hiroshi nohara" },
];
builder.queryType({
fields: (t) => ({
employees: t.field({
type: ["Employee"],
resolve: () => employees,
}),
employee: t.field({
type: "Employee",
nullable: true,
args: {
id: t.arg.int({ required: true }),
},
resolve: (_, args) => {
return employees.find((employee) => employee.id === args.id) ||
null;
},
}),
}),
});
builder.mutationType({
fields: (t) => ({
addEmployee: t.field({
type: "Employee",
args: {
name: t.arg.string({ required: true }),
},
resolve: (_, args) => {
const _employee = {
// カウントアップ
id: (employees && employees.length > 0
? employees?.at(-1)?.id as number
: 0) + 1,
name: args.name,
};
employees.push(_employee);
return _employee;
},
}),
}),
});
export const schema = builder.toSchema();
Remixで/graphqlアクセスができるように設定
loaderがNextjsでいうAPIRouteのGET、
actionがPOSTの役割を果たします。
// packages/web/app/routes/graphql.ts
import type { LoaderFunction } from "@remix-run/node";
import { createYoga } from "graphql-yoga";
import { schema } from "../graphql/schema";
const yoga = createYoga<{
req: Request;
}>({
graphqlEndpoint: "/graphql",
schema,
fetchAPI: globalThis,
});
export const loader: LoaderFunction = async ({ request }) => {
return yoga.handleRequest(request, { req: request });
};
export const action: LoaderFunction = async ({ request }) => {
return yoga.handleRequest(request, { req: request });
};
Graphqlをクライアント側から呼び出せるように
urqlでGraphqlリクエストができるようにコンテキストを作ります。
// packages/web/app/context/urql-provider.tsx
"use client";
import { cacheExchange, createClient, fetchExchange } from "@urql/core";
import type { ReactNode } from "react";
import { Provider } from "urql";
export default function UrqlProvider({ children }: { children: ReactNode }) {
const client = createClient({
url: "/graphql",
exchanges: [cacheExchange, fetchExchange],
});
return <Provider value={client}>{children}</Provider>;
}
root.tsxに適用します。
// packages/web/app/root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";
import "./tailwind.css";
import UrqlProvider from "./context/urql-provider";
export const links: LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return (
<UrqlProvider>
<Outlet />
</UrqlProvider>
);
}
クライアントからGraphQLリクエストして表示
これでRemixとしては設定終了です。
// packages/web/app/routes/_index.tsx
import type { MetaFunction } from "@remix-run/node";
import { gql, useQuery } from "urql";
import type { Employee } from "../graphql/schema";
export const meta: MetaFunction = () => {
return [
{ title: "New Remix App" },
{ name: "description", content: "Welcome to Remix!" },
];
};
interface EmployeeQueryResult {
employee: Employee;
}
interface EmployeeQueryVariables {
id: number;
}
export default function Index() {
const EmployeeQuery = gql`
query ($id: Int!) {
employee(id: $id) {
id
name
}
}
`;
const [result] = useQuery<EmployeeQueryResult, EmployeeQueryVariables>({
query: EmployeeQuery,
variables: {
id: 1,
},
});
if (result.fetching) return <p>Loading...</p>;
if (result.error) return <p>Error: {result.error.message}</p>;
return (
<div className="flex h-screen items-center justify-center">
<div className="flex flex-col items-center gap-16">
<header className="flex flex-col items-center gap-9">
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
Welcome to Remix_SST_Graphql Company
</h1>
</header>
<body>従業員1:{result.data?.employee.name}</body>
</div>
</div>
);
}
SSTの設定
sstではNextjsだけでなくRemixのデプロイも公式的に対応しています。
// sst.config.ts
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "sst",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
new sst.aws.Remix("MyWeb", {
path:"./packages/web",
});
},
});
デプロイ
※もちろんデプロイにはAWS cliの設定は済んでおく必要があります。
# ローカルで確認
npx sst dev
# デプロイして確認
npx sst deploy
# 削除(ローカル確認時も必ず行うこと)
npx sst remove
Discussion