Skip to content
On this page

Rick and Morty Example

This example is the beginning a FWA. With this lambda handler, IaC from SST, Arc, or similar one can simply render HTML in the lambda and return it to the client. Thought this example does not show it a database, like DynamoDB could also be added for another level of caching removing much of the need to hit the original API between invocations.

useRickAndMortyCharacters.ts

import { createSharedExecutionComposable } from '@serverless-use/core'

type Info = {
  count: number
  pages: number
  next: string | null
  prev: string | null
}

type Character = {
  id: number
  name: string
  status: string
  species: string
  type: string
  gender: string
  origin: {
    name: string
    url: string
  }
  location: {
    name: string
    url: string
  }
  image: string
  episode: string[]
  url: string
  created: string
}

type CharacterResponse = {
  info: Info
  results: Character[]
}

const API_BASE_URL = 'https://rickandmortyapi.com/api'
// Invocation level cache
const characters: Record<number, Character[]> = {}

export const useRickAndMortyCharacters = createSharedExecutionComposable(() => {
  return {
    getCharacters: async (page: number = 1) => {
      // If we haven't fetched this page yet, fetch it
      if (!characters[page]) {
        console.log('Making request for page', page)
        const response = await fetch(`${API_BASE_URL}/character/?page=${page}`)
        if (response.ok) {
          const data = (await response.json()) as CharacterResponse
          characters[page] = data.results
        }
      }
      return characters[page] || []
    },
  }
})

handler.ts

import { use, useQueryParameters, useResponseHeaders } from '@serverless-use/apigw'
import { useRickAndMortyCharacters } from '../composables/useRickAndMortyCharacters.ts'
import fetch from 'node-fetch'

export default use(async () => {
  const { set } = useResponseHeaders()
  const { get } = useQueryParameters<{ page?: string }>()
  const { getCharacters } = useRickAndMortyCharacters()

  const page = Math.max(1, parseInt(get('page') || '1'))
  const characters = await getCharacters(page)

  set('Content-Type', 'text/html')
  return /*html*/ `
      <div style="display: inline-flex; flex-direction: column; gap:1rem;">
        <div style="display: grid; grid-template-columns: repeat(4, max-content); gap: 1rem;">
          ${characters
            .map(
              (character) =>
                `<button onclick="updateCharacterImage('${character.image}')">${character.name}</button>`,
            )
            .join('')}
      </div>
      <div style="display: flex; justify-content: space-between;">
        <a 
          ${page <= 1 ? `style='opacity: .5;pointer-events:none;'` : ''} 
          href=/?page=${page - 1}>Previous</a>
        <a href=/?page=${page + 1}>Next</a>
      </div>
      <img style="width: 16rem;" id='character'/>
      
      <script>
        const charactersEl = document.getElementById('character')
        const updateCharacterImage = (url) => charactersEl.src=url
    </script>`
})