PlanetScaleとPrisma ORMによるサーバーレスAPIの構築と配備


もともとはNitric Site

何をするか


このガイドでは、Serverlessなフレームワークを使用してAPIを作成しますNitric . これは、Nine Serverlessな関数を使用しPlanetScale and Prisma データ持続性のために.その後、アプリケーションをお好みの雲に展開します.AWS , GCP , or Azure .
物事を点灯するには、APIを作成するイメージテンプレートのアップロードを許可することによって、テキストを生成すると、新しいイメージを生成することでmemesを生成します.あなたが構築したい他のAPIのための手順を適応させてください.

For image editing, we used a library from NPM called jimp, but you could use anything else you like.


完了したら、テンプレートイメージを受け入れるAPIを持っているし、そこから複数のミームを生成することができます.

必要条件


このガイドを完成するには、前もってセットアップを必要とすることがあります.
  • とのアカウントPlanetScale 無料です
  • Node.js

  • AWS , GCP or Azure 説明する
  • 始める


    新しい窒素プロジェクトから始めましょう.
    # create the project
    nitric new api-guide
    ? Choose a template:  [Use arrows to move, type to filter]
    > official/TypeScript - Starter
      official/JavaScript - Starter
    
    # navigate to the new project directory
    cd api-guide
    
    # install dependencies
    npm install
    
    一度プロジェクトを実行すると、ローカルで実行できます.
    nitric run
    
    例のアプリにはhello world スタイル例の関数.一度実行したらHTTPリクエストでテストできます.
    curl http://localhost:9001/apis/main/hello/John
    
    # expected response: Hello John
    
    # press Ctrl+C to stop the app
    

    Since we won't use the example function you can delete the functions/hello.ts file.


    イメージ編集のために後でGIMPを使うので、今もインストールしましょう.
    npm install jimp -save
    

    データベースとスキーマの設定


    次に、PlanetScaleデータベースが必要になります.アカウントが既にある場合は、次の手順に進みます.そうでなければ、あなたはsign up 無料のアカウント.

    データベースの作成


    PlanetScale CLIまたはWebダッシュボードを使用して新しいデータベースを作成できます.
    以下はCLIを使用した例です.
    pscale database create planetnitric --region us-east
    

    You can pick a different region for you database if you prefer, see: available regions


    プリズマセットアップ


    ここでセットアップPrismaに準備ができました.NPMでプロジェクトに追加してみましょう.
    npm install prisma --save-dev
    npm install @prisma/client@dev --save
    

    Note: @dev version of the prisma client is needed temporarily due to this issue, which has been resolved but not fully released. We'll update the guide to remove this step once that fix is fully released.


    そして、最初のスキーマファイルを生成し、最初のスキーマファイルを生成できます.
    npx prisma init
    
    これは、フォルダ内の新しいprismaスキーマを与えますprisma そして新しい.env Configを含むファイルは、我々のPlanetScaleデータベースに接続するために使用します.

    スキーマのビルド


    Prismaの内容を上書きします.スキーマ以下のスキーマ.これを使用してデータベースを初期化します.
    // prisma/schema.prisma
    
    // This is your Prisma schema file,
    // learn more about it in the docs: https://pris.ly/d/prisma-schema
    
    generator client {
      provider        = "prisma-client-js"
      previewFeatures = ["referentialIntegrity"]
      binaryTargets   = ["linux-musl"]
      output   = "./client"
    }
    
    datasource db {
      provider             = "mysql"
      url                  = env("DATABASE_URL")
      referentialIntegrity = "prisma"
    }
    
    model MemeTemplate {
      name          String         @id @unique
      createdAt     DateTime       @default(now())
      textPositions TextPosition[]
      Meme          Meme[]
    }
    
    model TextPosition {
      id       String       @id @default(cuid())
      name     String
      memeId   String
      posX     Int
      posY     Int
      width    Int
      height   Int
      template MemeTemplate @relation(fields: [memeId], references: [name], onDelete: Cascade)
    
      @@index([memeId])
    }
    
    model Meme {
      id         String       @id @default(cuid())
      createdAt  DateTime     @default(now())
      templateId String
      template   MemeTemplate @relation(fields: [templateId], references: [name], onDelete: Cascade)
    
      @@index([templateId])
    }
    
    次に、スキーマからPrismaクライアントを生成します.
    npx prisma generate
    
    最後に、ファイルを作成することによって、Prismaクライアントのインスタンスをインポートしやすくしましょうprisma/index.ts このコードを追加する
    import { PrismaClient } from './client';
    
    export * from './client';
    
    let prisma: PrismaClient;
    
    if (process.env.NITRIC_ENVIRONMENT !== 'build') {
      if (process.env.NODE_ENV === 'production') {
        prisma = new PrismaClient({
          errorFormat: 'minimal',
        });
      } else {
        globalThis['prisma'] =
          globalThis['prisma'] ||
          new PrismaClient({
            errorFormat: 'pretty',
          });
        prisma = globalThis['prisma'];
      }
    }
    
    export default prisma;
    

    プラネタリウムへの接続


    現在、我々のスキーマは準備ができています.最も簡単な方法はconnect ボタンをクリックして選択しますPrisma ドロップダウンから.これは、あなたがコピーすることができます値を与える.env プロジェクトのために.

    終わったら.env ファイルは次のようになります.
    DATABASE_URL='<Your URL from the above screenshot>'
    
    スキーマが使用可能で、接続の詳細が設定されている場合は、PreSchemaスキーマをPlanetScaleにプッシュできます.
    npx prisma db push
    

    アプリケーションにクラウドリソースを追加


    アプリケーションは、コード内のリソースを定義してビルドすると、任意のルートでこれを書くことができます.js or .ts ファイル.物事を整理するには、一緒にリソースをグループ化をお勧めします.では、新しいAPIをサポートするリソースを定義することから始めましょうresources ディレクトリ.
    まず、APIゲートウェイを宣言しましょう.新しいファイルを作成するapis.ts 新しいフォルダにresources とこのコード:
    // resources/apis.ts
    import { api } from '@nitric/sdk';
    
    export const memeApi = api('meme');
    
    これは新しいapi 「meme」という名前のリソースで、プロジェクト内の他の場所で参照できるリソースとしてエクスポートします.
    次に、私たちのmemeイメージファイルを格納するためにいくつかのバケツを作成しましょう.新しいファイルを作成するbuckets.tsresources そして以下のように設定します:
    // resources/buckets.ts
    import { bucket } from '@nitric/sdk';
    
    export const templates = bucket('templates');
    export const memes = bucket('memes');
    
    再び、我々は、このケースでは、新しいリソースを、バケットを宣言していると、アプリケーション内で一意の名前を与えている.彼らは繰り返し宣言されることなく再び参照することができますので、我々はそれらのリソースをエクスポートします.

    テンプレートの作成


    リソースが宣言されたので、最初のサービスを作成しましょう.このサービスでは、APIコンシューマーは、ミームとベースの設定可能なセットのベースイメージを提供することによって新しいミームテンプレートを登録することができます.
    /functions ディレクトリと呼ばれる新しいファイルを作成するtemplates.ts 次のコードを入力します.
    // functions/templates.ts
    import Jimp from 'jimp';
    import prisma, { MemeTemplate, TextPosition } from '../prisma';
    import { memeApi } from '../resources/apis';
    import { templates } from '../resources/buckets';
    
    export interface CreateTemplateRequest
      extends Omit<MemeTemplate, 'filepath' | 'createdAt'> {
      source: string;
      textPositions: Omit<TextPosition, 'id' | 'memeId'>[];
    }
    
    const templateImgs = templates.for('writing');
    
    export const normalizeName = (name: string) => {
      return name.replace(' ', '-').replace(/[^\w-]*/g, '');
    };
    
    // POST: /templates - Create new meme templates
    memeApi.post('/templates', async ({ req, res }) => {
      const {
        textPositions,
        source,
        name: rawName,
      } = req.json() as CreateTemplateRequest;
      const name = normalizeName(rawName);
      const img = await Jimp.read(source);
    
      try {
        const template = await prisma.memeTemplate.create({
          data: {
            name,
            textPositions: {
              create: textPositions,
            },
          },
        });
    
        // Limit width to 512px max to save space
        const resizeFactor = 512 / img.getWidth();
        img.resize(img.getWidth() * resizeFactor, img.getHeight() * resizeFactor);
    
        // store the image in the bucket
        const buf = await img.getBufferAsync(img.getMIME());
        await templateImgs.file(name).write(buf);
    
        res.json(template);
      } catch (e) {
        res.status = 409;
        res.body = `Name already taken: ${name}: ${e.message}`;
      }
    });
    
    // GET: /templates - List all meme templates
    memeApi.get('/templates', async ({ res }) => {
      const memeTemplates = await prisma.memeTemplate.findMany({
        include: {
          textPositions: true,
        },
      });
    
      res.json(memeTemplates);
    });
    
    この例では、APIゲートウェイをインポートしていますmemeApi 我々は、我々の中でつくりましたresources ディレクトリ、および方法を使用してルートとメソッドハンドラを登録するget and post , あなたのようなフレームワークのようなExpress .
    また、テンプレート画像を保存するために使用されるバケットをインポートしているtemplateImages リソースディレクトリから.また、バケットの我々の意図した使用を宣言するfor あなたのコードがどのようなアクセス許可を必要とし、展開中にそれらを適用できるかをNoneにします.この例では、我々は我々のテンプレートサービスを与えているだけですwrite テンプレートバケツへのアクセス.
    着信context オブジェクト(req and res ) Path Params , Query Params , Headers , body , statusなどのリクエストと応答の詳細を含んでいます.

    ミームサービス


    このtemplates 例:別の新しいファイルを作成しますfunctions/memes.ts , 次のコードを使用します.
    // functions/memes.ts
    import { FileMode } from '@nitric/sdk';
    import Jimp from 'jimp';
    import prisma, { Meme } from '../prisma';
    import { memes, templates } from '../resources/buckets';
    import { memeApi } from '../resources/apis';
    
    interface MemeCreationRequest extends Omit<Meme, 'id' | 'templateId'> {
      templateName: string;
      texts: {
        name: string;
        value: string;
      }[];
    }
    
    const templateImgs = templates.for('reading');
    const memesImgs = memes.for('reading', 'writing');
    
    // POST: /memes - Create new meme images
    memeApi.post('/memes', async ({ req, res }) => {
      const meme = req.json() as MemeCreationRequest;
    
      const template = await prisma.memeTemplate.findFirst({
        include: {
          textPositions: true,
        },
        where: {
          name: {
            equals: meme.templateName,
          },
        },
      });
    
      const imgBytes = await templateImgs.file(template.name).read();
    
      // Load the image and font
      const [img, font] = await Promise.all([
        Jimp.read(Buffer.from(imgBytes)),
        Jimp.loadFont(Jimp.FONT_SANS_32_WHITE),
      ]);
    
      // Apply text to the template image to create the meme
      meme.texts.forEach((text) => {
        // get the text template
        const matchingText = template.textPositions.find(
          (tp) => tp.name === text.name
        );
    
        if (!matchingText) return;
    
        // ignore if anchor tags don't match
        img.print(
          font,
          img.getWidth() * (matchingText.posX / 100),
          img.getHeight() * (matchingText.posY / 100),
          {
            text: text.value,
            alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
            alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE,
          },
          img.getWidth() * (matchingText.width / 100),
          img.getHeight() * (matchingText.height / 100)
        );
      });
    
      const [newMeme, buf] = await Promise.all([
        prisma.meme.create({
          data: {
            templateId: meme.templateName,
          },
        }),
        img.getBufferAsync(Jimp.MIME_PNG),
      ]);
    
      await memesImgs.file(newMeme.id).write(buf);
    
      res.json(newMeme);
    });
    
    // GET: /memes - List all created memes
    memeApi.get('/memes', async ({ res }) => {
      const memes = await prisma.meme.findMany();
      return res.json(memes);
    });
    
    // GET: /memes/:id - Get a meme image by it's ID
    memeApi.get('/memes/:id', async ({ req, res }) => {
      const { id } = req.params;
      const signedUrl = await memesImgs.file(id).signUrl(FileMode.Read);
      res.status = 303;
      res.headers['Location'] = [signedUrl];
    });
    
    繰り返しますが、このファイルはmemeApi リソースではなく、/memes パス.
    また、リクエストread アクセスtemplateImages バケットread-write アクセスmemeImages バケット.

    ローカルテスト


    APIを確立したので、ローカルでテストしましょう.
    nitric run
    
    いくつかのスピナーが表示されますが、すべての設定を取得します.
    SUCCESS Configuration gathered (6s)
    SUCCESS Created Dev Image! (0s)
    SUCCESS Started Local Services! (4s)
    SUCCESS Started Functions! (1s)
    
    Api  | Endpoint
    meme | http://localhost:9001/apis/meme
    
    ローカルでAPIを走らせるとき、Nailは彼らの名前で彼らを下位ルートにします.したがって、この例では、新しいミームテンプレートを作成するには、あなたのPOST リクエストhttps://localhost:9001/apis/meme/templates .
    以下はAPIをテストするためのリクエスト例です.

    テンプレートを作成する


    curl -X POST http://localhost:9001/apis/meme/templates \
       -H 'Content-Type: application/json' \
       -d '{"name":"my-meme","source":"https://www.meme-arsenal.com/memes/89f28a7e83e28f15b1d8e560c788b4fc.jpg","textPositions":[{"name":"topText","posX":50,"posY":0,"width":50,"height":50},{"name":"bottomText","posX":50,"posY":50,"width":50,"height":50}]}'
    
    フルリクエスト
    {
      "name": "my-meme",
      "source": "https://www.meme-arsenal.com/memes/89f28a7e83e28f15b1d8e560c788b4fc.jpg",
      "textPositions": [
        {
          "name": "topText",
          "posX": 50,
          "posY": 0,
          "width": 50,
          "height": 50
        },
        {
          "name": "bottomText",
          "posX": 50,
          "posY": 50,
          "width": 50,
          "height": 50
        }
      ]
    }
    

    For source provide a URL hosting a meme template image in a common format like .png or .jpg


    テンプレートを使用して新しいmemeを作成します


    curl -X POST http://localhost:9001/apis/meme/memes \
       -H 'Content-Type: application/json' \
       -d '{"templateName":"my-meme","texts":[{"name":"topText","value":"top text content"},{"name":"bottomText","value":"bottom text content"}]}'
    
    フルリクエスト
    {
      "templateName": "my-meme",
      "texts": [
        {
          "name": "topText",
          "value": "top text content"
        },
        {
          "name": "bottomText",
          "value": "bottom text content"
        }
      ]
    }
    

    画像を取得する


    前の要求から返されたmeme idを使用してブラウザを開き、http://localhost:9001/apis/meme/memes/<Meme ID> .

    クラウドに配備する


    準備ができたら、このプロジェクトをAWS、AzureまたはGoogleクラウドに配備できます.この例では、AWSの手順を示しますが、すべてのケースで本質的に同じです.
    を定義するstack . スタックは本質的に展開ターゲットと呼ばれ、クラウドで実行されているアプリケーションのインスタンスを表します.
    実行して新しいスタックを作成できますnitric stack new そして、プロンプトの後に.この場合、スタックを呼び出しますawsdev , 絹篩で篩うたようaws ターゲットクラウドとしてus-east-1 ターゲット領域として:
    nitric stack new
    ? What do you want to call your new stack? awsdev
    ? Which Cloud do you wish to deploy to? aws
    ? select the region us-east-1
    
    最後にup スタックを展開し、コードをクラウドにプッシュするコマンド
    nitric up -s awsdev
    
    から返されるURLを使用できますup あなたの新しく配備されたAPIに要求をするコマンド.次に、完了したら、スタックを破壊することができますdown コマンド
    nitric down -s awsdev
    
    そして、thats!
    あなたがこのガイドに質問やフィードバックを持っているなら、私はあなたから聞いてみたい.コメント、またはGitHub