DEV Community

Cover image for Introducing the BFF (Backend for Frontend) Concept by simple application with SvelteKit, Supabase, and GraphQL Code Generator
soom
soom

Posted on • Edited on

5 1 1 2 2

Introducing the BFF (Backend for Frontend) Concept by simple application with SvelteKit, Supabase, and GraphQL Code Generator

Abstract

Introducing the concept of BFF (Backend for Frontend) through a Simple Application with SvelteKit and Supabase.

BFF (Backend for Frontend). Functional Server Block을 조합하여 Frontend 친화적인 Server Layer 를 구현, 이를 통해 개발의 효율성을 늘린다 라는 개념. 그러나 사실상 MicroService Architecture 가 기반이 되는 큰 단위의 서비스에 한해서나 의미가 있는 개념이다.

그렇다고해도 이러한 구조적인 개념을 소규모 프로젝트에 적용시키는 것이 불가능한 일은 아니다.
이번 포스팅에서는 Full Stack Application인 SvelteKit 과 BaaS인 Supabase 를 통해 유사한 개념을 미리 소규모로 테스트해보는 내용을 소개하고자한다.

이때, DB와 Functional Server Block의 역할을 오픈 소스 BaaS (Backend as a Service)Supabase 로, Frontend 와 BFF 역할은 Svelte-kit으로 구현하였다.

또한, 기존의 REST 방식과는 다른 GraphQL Codegen 을 이용한 서버와 클라이언트간의 소통 방법을 소개하고자 한다.

이 포스팅을 읽기 전에 하기 포스팅을 읽고 오는 것을 권장한다.

https://dev.to/soom/how-to-use-graphql-and-react-query-with-graphql-code-generator-based-on-nextjs-23jj


Getting Started

Setting up Supabase

Supabase 가입 후 Project 생성 뒤 Dashboard 진입

본 포스팅에서는 DB 스키마 및 테이블을 따로 작성하지 않고 Quick Start 예제를 들고와서 작업

SQL Editor > Countries > Run 을 통해 DB 생성 후 Table Editor 선택해서 데이터 확인

supabase1

supabase2

supabase3


개별 데이터 조회 위해 function 생성 (Supabase feature)

SQL Editor > New Query 에서 다음 명령어 실행 후 확인

SQL Editor > New Query
/* Create Function */
create or replace function get_country()
returns setof countries
language sql
as $$
  select * from countries;
$$;

/* Check Result */
select *
from get_country()
where id = 1;
Enter fullscreen mode Exit fullscreen mode

Setting up SvelteKit

원하는 프로젝트 폴더를 생성한 뒤 SvelteKit 프로젝트를 생성

pnpm create svelte@latest

✔ Where should we create your project?
  (leave blank to use current directory) … simple-bff
✔ Which Svelte app template? › Skeleton project
✔ Add type checking with TypeScript? › Yes, using TypeScript syntax
✔ Add ESLint for code linting? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
? Add Playwright for browser testing? › No / Yes
Enter fullscreen mode Exit fullscreen mode

Setting up Supabase Client on SvelteKit

Supabase 클라이언트를 설치

Terminal
pnpm add -S @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

src/lib/db/index.ts 에 다음과 같이 Supabase 클라이언트 정의

src/lib/db/index.ts
import { createClient } from '@supabase/supabase-js';
import { ANON_KEY, PROJECT } from '$env/static/private'; // load from env

const supabase = createClient(`https://${PROJECT}.supabase.co`, ANON_KEY);

export default supabase;
Enter fullscreen mode Exit fullscreen mode

Note

클라이언트 정보는 Supabase Dashboard 에서 Settings > API > URL, Project API Keys 확인

api-keys


src/lib/country/db/index.ts, src/lib/countries/db/index.ts 에 해당하는 DB 요청 정의

요청한 DB 데이터가 잘 불러오지는지 확인

src/lib/country/db/index.ts
import supabase from '$lib/db';

// request id = 18 country data
const country = await supabase.rpc('get_country').eq('id', 18);

export default country;
Enter fullscreen mode Exit fullscreen mode
src/lib/countries/db/index.ts
import supabase from '$lib/db';

// request all the countries
const countries = await supabase.from('countries').select();

export default countries;
Enter fullscreen mode Exit fullscreen mode

Setting up Graphql Yoga Server on SvelteKit Server

SvelteKit Server 구성. 여기서는 Graphql Yoga 를 이용해서 Graphql Server 를 구현

src/routes/graphql/+server.ts에 서버 파일 작성

server api url 은 /graphql 로 접속 가능하다

Note

  • 이 프로젝트는 Domain Base Structure 이기 때문에 각각 해당하는 graphql query, schema 가 산재되어 있음

  • graphql file loader 는 프로젝트 내 퍼져있는 graphql 파일을 한꺼 번에 모아줌

  • 여기서 country.data[0], countries.dataSupabase 의 요청 리턴값

  • 기타 나머지 부분에 대한 내용은 링크 참조:
    https://the-guild.dev/graphql/yoga-server/docs/integrations/integration-with-sveltekit

Terminal
pnpm add -S graphql graphql-yoga

pnpm add -D @graphql-tools/graphql-file-loader @graphql-tools/load
Enter fullscreen mode Exit fullscreen mode
src/routes/graphql/+server.ts
import { createYoga, createSchema } from 'graphql-yoga';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { loadSchema, loadDocuments } from '@graphql-tools/load';
import country from '$lib/country/db';
import countries from '$lib/countries/db';

import type { RequestEvent } from '@sveltejs/kit';

const typeDefs = await loadSchema('./src/lib/**/graphql/schema.graphql', {
    loaders: [new GraphQLFileLoader()]
});

const defaultQuery = await loadDocuments('./src/lib/countries/graphql/query.graphql', {
    loaders: [new GraphQLFileLoader()]
}).then((res) => res[0].rawSDL);

const yogaApp = createYoga<RequestEvent>({
    schema: createSchema({
        typeDefs,
        resolvers: {
            Query: {
                countries: () => countries.data,
                country: () => country.data[0]
            }
        }
    }),
    graphiql: {
        defaultQuery
    },
    fetchAPI: globalThis
});

export { yogaApp as GET, yogaApp as POST };
Enter fullscreen mode Exit fullscreen mode

각각 해당하는 graphql schema와 query 파일 생성 (country, countries)

src/lib/country/graphql/schema.graphql
type Country {
    id: Int!
    name: String!
    iso2: String!
    iso3: String!
    local_name: String
    continent: String
}

type Query {
    country: Country
}
Enter fullscreen mode Exit fullscreen mode
src/lib/country/graphql/query.graphql
query Country {
    country {
        id
        name
        iso2
        iso3
        local_name
        continent
    }
}
Enter fullscreen mode Exit fullscreen mode
src/lib/countries/graphql/schema.graphql
type Query {
    countries: [Country]!
}
Enter fullscreen mode Exit fullscreen mode
src/lib/countries/graphql/query.graphql
query Countries {
    countries {
        id
        name
        iso2
        iso3
        local_name
        continent
    }
}
Enter fullscreen mode Exit fullscreen mode

이제 실행시켜서 /graphql에 접속하여 playground 가 정상적으로 나타나는지 확인

query


Auto Generating Svelte Query with Graphql Codegen

codegen 관련 패키지 설치

Terminal
pnpm add -S graphql-request

pnpm add -D @graphql-codegen/cli @graphql-codegen/near-operation-file-preset @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-query

pnpm add -D @sveltestack/svelte-query
Enter fullscreen mode Exit fullscreen mode

Note

@graphql-codegen/typescript-react-query 설치 근거

  • graphql codegen 에서는 svelte query 는 지원 하지 않음
  • 다만, tanstack에서 제공하는 react query, svelte query 는 naming convention 만 다를뿐 구조가 동일하기에 이 부분에 대한 수정만 해주면 사용 가능함

root 폴더에 codegen.yml 작성

package.json 에 codegen script 도 작성

Note

codegen.yml
schema: http://localhost:5173/graphql
documents: './src/lib/**/graphql/!(*.generated).{gql,graphql}'
require:
  - ts-node/register
generates:
  src/lib/types/index.ts:
    plugins:
      - typescript

  src/:
    preset: near-operation-file
    presetConfig:
      extension: .generated.ts
      baseTypesPath: 'lib/types'

    plugins:
      - typescript-operations
      - typescript-react-query
    config:
      pureMagicComment: true
      exposeQueryKeys: true
      exposeFetcher: true
      withHooks: true
      fetcher: graphql-request

config:
  interfacePrefix: 'I'
  typesPrefix: 'I'
  skipTypename: true
  declarationKind: 'interface'
  noNamespaces: true

hooks:
  afterOneFileWrite: 'prettier --plugin-search-dir . --write .'
Enter fullscreen mode Exit fullscreen mode
package.json
{
  ...
"scripts": {
        ...
        "codegen": "graphql-codegen --config codegen.yml"
    },
  ...
}
Enter fullscreen mode Exit fullscreen mode

svelte query 기본 환경 설정

src/lib/plugin/svelteQuery.ts
import { QueryClient } from '@sveltestack/svelte-query';

// Configure for static fetching from Server
export const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnMount: false,
            refetchOnWindowFocus: false,
            refetchOnReconnect: false
        }
    }
});
Enter fullscreen mode Exit fullscreen mode
src/routes/+layout.svelte
<script lang="ts">
    import '../app.css';
    import { queryClient } from '$lib/plugin/svelteQuery';
    import { QueryClientProvider } from '@sveltestack/svelte-query';
</script>

<QueryClientProvider client={queryClient}>
    <main class="flex justify-center items-center w-full h-[100vh]">
        <slot />
    </main>
</QueryClientProvider>
Enter fullscreen mode Exit fullscreen mode

codegen 실행 후 결과 확인

Terminal
# first, run svelte-kit app with server
pnpm dev

# auto generating!
pnpm codegen

> graphql-codegen --config codegen.yml

✔ Parse Configuration
✔ Generate outputs
Enter fullscreen mode Exit fullscreen mode

src/lib/country/graphql, src/lib/countries/graphql 폴더에 query.generated.ts 생성

src/lib/types/index.ts 파일 생성

여기서 타입은 정상적으로 생성되나 문제는 query.generated.ts

위에서 언급한대로 react-query 기반으로 생성했기에 svelte-query 에 맞게 수정이 필요하다.

아래 예시처럼 react-query import package를 svelte query에 맞게 정리

src/lib/country/graphql/query.generated.ts
// auto-generated react-query
// import * as Types from '../../types';

// import { GraphQLClient } from 'graphql-request';
// import { RequestInit } from 'graphql-request/dist/types.dom';
// import { useQuery, UseQueryOptions } from '@tanstack/react-query';

// Convert for svelte-query
import type * as Types from '$lib/types';

import { GraphQLClient } from 'graphql-request';
import { useQuery } from '@sveltestack/svelte-query';

import type { RequestInit } from 'graphql-request/dist/types.dom';
import type { UseQueryOptions } from '@sveltestack/svelte-query';
Enter fullscreen mode Exit fullscreen mode

Setting up Frontend

이제 view 레벨 작성

SPA 는 마운트되면서 빈 index.html 에 js 패키지를 로딩하는 식으로 렌더링을 하기 때문에 SEO 에 굉장히 불리하나 SSRHydration technique을 통해 필요한 데이터를 미리 로딩하여 SEO 에 장점을 가져갈수 있다

Note

src/routes/country/+page.ts
import { error } from '@sveltejs/kit';
import { dehydrate } from '@sveltestack/svelte-query';
import { queryClient } from '$lib/plugin/svelteQuery';
import { useCountryQuery } from '$lib/country/graphql/query.generated';
import { GraphQLClient } from 'graphql-request';

import type { PageLoad } from './$types';

export const load = (async ({ route }) => {
    const gqlClient = new GraphQLClient('http://localhost:5173/graphql');

    await queryClient.prefetchQuery(useCountryQuery.getKey(), useCountryQuery.fetcher(gqlClient));

    if (route.id === '/country') {
        return {
            title: 'country',
            dehydratedState: dehydrate(queryClient)
        };
    }

    throw error(404, 'Not found');
}) satisfies PageLoad;
Enter fullscreen mode Exit fullscreen mode
src/routes/country/+page.svelte
<script lang="ts">
    import { GraphQLClient } from 'graphql-request';
    import { useCountryQuery } from '$lib/country/graphql/query.generated';
    import type { PageData } from './$types';

    export let data: PageData;

    const gqlClient = new GraphQLClient('http://localhost:5173/graphql');

    const countriesQueryResult = useCountryQuery(gqlClient);
    const { country } = $countriesQueryResult.data!;
</script>

<svelte:head>
    <title>{data.title}</title>
</svelte:head>

<div class="text-center">
    <h1 class="text-3xl font-bold">Hello Country!</h1>
    <a href="/"> > Back Home</a>
    <div class="my-2">
        {country?.iso2}
        {country?.name}
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Result

👉 CodeSandBox Sample Link

Result


Conclusion

본 포스팅에서는 BFF 간단한 개념 모델을 Supabase, SvelteKit으로 구현해보았다.

큰 규모의 서비스에서는 Client, Server(BFF), Server, DB 등을 다 따로 나눠서 구성하겠지만 소규모의 간단한 예제를 통해서도 개념 자체를 이해하는데는 큰 무리가 없다.

또한, GraphQL Code Generator를 이용해 Server 와 Client 간의 의사소통을 자동 생성으로 접근하는 방법을 소개하였다.

기존의 Swagger 와 같은 방식으로는 여전히 Client 와 Server 의 의사소통에는 한계가 있었다. 그러나 GQL Codegen을 통해 자동 생성되는 Schema를 이용하는 방식은 Server 쪽에 따로 문의할 필요없이 Human Error 를 최소화할 수 있기에 적극 권장하는 방법이다.

p.s
대부분의 GraphQL 의 서버나 클라이언트는 Apollo Server, Apollo Client 등을 이용하지만 여기서는 GraphQL Yoga, Svelte Query 를 이용하는 방법을 소개하였다.
GraphQL Yoga 가 v3 로 넘어오면서 Apollo Server의 불편함을 크게 해결한 솔루션을 제공하고 있는데다 이를 지원하는 프로젝트인 GraphQL Guild가 워낙 GraphQL에 진심이기 때문에 앞으로도 큰 발전이 기대 가능하다.

The Guild URL: https://the-guild.dev/

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More