Skip to content

Instantly share code, notes, and snippets.

@devinschumacher
Created November 15, 2024 17:39
Show Gist options
  • Save devinschumacher/ee4520f8a93fd5b675a6669e87620753 to your computer and use it in GitHub Desktop.
Save devinschumacher/ee4520f8a93fd5b675a6669e87620753 to your computer and use it in GitHub Desktop.
Nuxt Data Fetching - `server/api` + `composables`

Data fetching in Nuxt server/api/

Checklist:

  1. types
  2. endpoints
  3. composables
  4. tests

Types

  • create types for the endpoints/data shapes

API Endpoints

  • create server API endpoints in server/api/*.ts, and include the request method in the filename (example.get.ts, example.post.ts)
  • get route information from the (event) object - event.context.params

Composables

  • make the data fetching functions in composables/*.ts
  • include error handling
  • use caching (sessionStorage) to cache the requested data on the client (so if user's navigate between already visited URLs the data is loaded from their browser cache instead of being requested again from our API). Can put this "cached fetch" in its own compsoable.
  • validate the data in your server routes
  • only fetch the necessary data needed to hydrate the page (pick, etc.)
  • use drizzle-zod for API input validation

API Tests

Types of API tests:

  1. Unit tests / Endpoint tests - to call & test individual endpoints w/ various inputs and verify the expected results. Make sure to test both positive & negative scenarios.
  2. Integration tests - test certain workflows (aka multiple steps).
  3. Contract tests - to verify the API is in-line with the provider contract. Data points haven't changed, data types haven't changed, data shape hasn't changed, etc.
  4. Security tests
  5. Performance tests
  6. Data-driven tests (test parameterization).

Unit tests:

  • test that endpoints work (status code)
  • test the endpoint only works with the intended request type
  • test the response data shape is as expected (.jsonSchema)
  • the response contains the expected data
  • the header information contains expected data
  • response time & size
  • only returns what it needs (not extra/unused information)
  • there is a max on the amount of data able to be requested

Integration tests & more:

  • use postman to create 'collections' for the endpoints and then auto generate tests for them (right click on collection > generate tests).

Contract tests:

  • Create a 'collection' of tests in postman that test all the API's configurations - run the tests on any/every update/change to the API.
  • Have the contract documented (API Specification file)
  • Use postman to auto generate contract tests (postman -> searchbar -> 'generate contract tests')

Tip: utilize postman interceptor to save all requests to the API and from there genereate the API specification (if you didn't start with the schema).. then create your tests to do the asserts/verifications.

API Monitors:

  • setup API monitoring to run against the production API and alert results

Test CI & reporting:

  • Run in CI pipeline / builds
  • Include performance tests in CI

Performance testing:

  • Processing load (response time, response size)
  • Memory load
  • Connection load
    • Spike load
    • Ramp load
    • Endurance load

Swipe files / examples

composables

// composables/useFetchWithCache.ts
import { StorageSerializers } from '@vueuse/core';

export default async <T>(url: string) => {
  // Use sessionStorage to cache the lesson data
  const cached = useSessionStorage<T>(url, null, {
    // By passing null as default it can't automatically
    // determine which serializer to use
    serializer: StorageSerializers.object,
  });

  if (!cached.value) {
    const { data, error } = await useFetch<T>(url, {
      headers: useRequestHeaders(['cookie']),
    });

    if (error.value) {
      throw createError({
        ...error.value,
        statusMessage: `Could not fetch data from ${url}`,
      });
    }

    cached.value = data.value as T;
  } else {
    console.log(`Getting value from cache for ${url}`);
  }

  return cached;
};
// composables/useFirstLesson.ts
export default async () => {
  const course = await useCourse();
  return course.value.chapters[0].lessons[0];
};

types

// types/course.ts
import { Lesson } from '@prisma/client';

export type LessonWithPath = Lesson & {
  path: string;
};

export type ChapterProgress = {
  [key: string]: boolean;
};

export type CourseProgress = {
  [key: string]: ChapterProgress;
};

API endpoints

// server/api/course/chapter/[chapterSlug]/lesson/[lessonSlug].get.ts
import { PrismaClient } from '@prisma/client';
import protectRoute from '~/server/utils/protectRoute';

const prisma = new PrismaClient();

export default defineEventHandler(async (event) => {
  // We allow users to access the first lesson without being logged in
  if (event.context.params.chapterSlug !== '1-chapter-1') {
    protectRoute(event);
  }

  const { chapterSlug, lessonSlug } = event.context.params;

  const lesson = await prisma.lesson.findFirst({
    where: {
      slug: lessonSlug,
      Chapter: {
        slug: chapterSlug,
      },
    },
  });

  if (!lesson) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Lesson not found',
    });
  }

  return {
    ...lesson,
    path: `/course/chapter/${chapterSlug}/lesson/${lessonSlug}`,
  };
});
// server/api/course/meta.get.ts
import { PrismaClient, Prisma } from '@prisma/client';

const prisma = new PrismaClient();

const lessonSelect = Prisma.validator<Prisma.LessonArgs>()({
  select: {
    title: true,
    slug: true,
    number: true,
  },
});
export type LessonOutline = Prisma.LessonGetPayload<
  typeof lessonSelect
> & {
  path: string;
};

const chapterSelect =
  Prisma.validator<Prisma.ChapterArgs>()({
    select: {
      title: true,
      slug: true,
      number: true,
      lessons: lessonSelect,
    },
  });
export type ChapterOutline = Omit<
  Prisma.ChapterGetPayload<typeof chapterSelect>,
  'lessons'
> & {
  lessons: LessonOutline[];
};

const courseSelect = Prisma.validator<Prisma.CourseArgs>()({
  select: {
    title: true,
    chapters: chapterSelect,
  },
});
export type CourseOutline = Omit<
  Prisma.CourseGetPayload<typeof courseSelect>,
  'chapters'
> & {
  chapters: ChapterOutline[];
};

export default defineEventHandler(
  async (): Promise<CourseOutline> => {
    const outline = await prisma.course.findFirst(
      courseSelect
    );

    // Error if there is no course
    if (!outline) {
      throw createError({
        statusCode: 404,
        statusMessage: 'Course not found',
      });
    }

    // Map the outline so we can add a path to each lesson
    const chapters = outline.chapters.map((chapter) => ({
      ...chapter,
      lessons: chapter.lessons.map((lesson) => ({
        ...lesson,
        path: `/course/chapter/${chapter.slug}/lesson/${lesson.slug}`,
      })),
    }));

    return {
      ...outline,
      chapters,
    };
  }
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment