Skip to content
Productivity
Skill

/fullstack-workflow

Complete fullstack workflow combining GET API routes, server actions, SWR data fetching, and form handling. Use when building features that need both data fetching and mutations from API to UI.

BOOST
From plugin
inbox-zero
12k25 skills2 agents
Install
$ npx -y skills add elie222/inbox-zero --skill fullstack-workflow --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/fullstack-workflow

Context preview

The summary Claude sees to decide when to auto-load this skill.

Complete fullstack workflow combining GET API routes, server actions, SWR data fetching, and form handling. Use when building features that need both data fetching and mutations from API to UI.

SKILL.md

fullstack-workflow.SKILL.md
name: fullstack-workflow
description: Complete fullstack workflow combining GET API routes, server actions, SWR data fetching, and form handling. Use when building features that need both data fetching and mutations from API to UI.

Fullstack Workflow

Complete guide for building features from API to UI, combining GET API routes, data fetching, form handling, and server actions.

Overview

When building a new feature, follow this pattern: 1. **GET API Route** - For fetching data 2. **Server Action** - For mutations (create/update/delete) 3. **Data Fetching** - Using SWR on the client 4. **Form Handling** - Using React Hook Form with Zod validation

1. GET API Route

For fetching data. Always wrap with `withAuth` or `withEmailAccount`:

// apps/web/app/api/user/example/route.ts
import { NextResponse } from "next/server";
import prisma from "@/utils/prisma";
import { withEmailAccount } from "@/utils/middleware";

// Auto-generate response type for client use
export type GetExampleResponse = Awaited<ReturnType<typeof getData>>;

export const GET = withEmailAccount(async (request) => {
  const { emailAccountId } = request.auth;
  
  const result = await getData({ emailAccountId });
  return NextResponse.json(result);
});

// We make this its own function so we can infer the return type for a type-safe response on the client
async function getData({ emailAccountId }: { emailAccountId: string }) {
  const items = await prisma.example.findMany({
    where: { emailAccountId },
  });

  return { items };
}

2. Server Action

For mutations. Use `next-safe-action` with proper validation.

**Action clients** (defined in `apps/web/utils/actions/safe-action.ts`):

| Client | Context | Use when | |--------|---------|----------| | `actionClientUser` | `ctx.userId` | Only need authenticated user | | `actionClient` | `ctx.emailAccountId`, `ctx.userId` | Need user + email account (most mutations) | | `adminActionClient` | `ctx.logger` | Admin-only actions (no userId in ctx) |

Always use `.metadata({ name: "actionName" })` for Sentry instrumentation. Use `SafeError` for expected errors.

**Validation Schema** (`apps/web/utils/actions/example.validation.ts`):

import { z } from "zod";

export const createExampleBody = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email"),
  description: z.string().optional(),
});
export type CreateExampleBody = z.infer<typeof createExampleBody>;

export const updateExampleBody = z.object({
  id: z.string(),
  name: z.string().optional(),
  email: z.string().email().optional(),
  description: z.string().optional(),
});
export type UpdateExampleBody = z.infer<typeof updateExampleBody>;

**Server Action** (`apps/web/utils/actions/example.ts`):

"use server";

import { actionClient } from "@/utils/actions/safe-action";
import { createExampleBody, updateExampleBody } from "@/utils/actions/example.validation";
import prisma from "@/utils/prisma";

export const createExampleAction = actionClient
  .metadata({ name: "createExample" })
  .inputSchema(createExampleBody)
  .action(async ({ 
    ctx: { emailAccountId }, 
    parsedInput: { name, email, description } 
  }) => {
    const example = await prisma.example.create({
      data: {
        name,
        email,
        description,
        emailAccountId,
      },
    });
    
    return example;
  });

export const updateExampleAction = actionClient
  .metadata({ name: "updateExample" })
  .inputSchema(updateExampleBody)
  .action(async ({ 
    ctx: { emailAccountId }, 
    parsedInput: { id, name, email, description } 
  }) => {
    const example = await prisma.example.update({
      where: { id, emailAccountId },
      data: { name, email, description },
    });
    
    return example;
  });

3. Data Fetching

Use SWR for client-side data fetching:

import useSWR from "swr";
import { GetExampleResponse } from "@/app/api/user/example/route";

export function useExamples() {
  return useSWR<GetExampleResponse>("/api/user/example");
}

4. Form Handling

Use React Hook Form with `useAction` from `next-safe-action/hooks`:

import { useCallback } from "react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { useAction } from "next-safe-action/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@/components/Input";
import { Button } from "@/components/ui/button";
import { toastSuccess, toastError } from "@/components/Toast";
import { getActionErrorMessage } from "@/utils/error";
import { createExampleAction } from "@/utils/actions/example";
import { createExampleBody, type CreateExampleBody } from "@/utils/actions/example.validation";

export function ExampleForm({ onSuccess }: { onSuccess?: () => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm<CreateExampleBody>({
    resolver: zodResolver(createExampleBody),
  });

  const { execute, isExecuting } = useAction(createExampleAction, {
    onSuccess: () => {
      toastSuccess({ description: "Example created!" });
      reset();
      onSuccess?.();
    },
    onError: (error) => {
      toastError({
        description: getActionErrorMessage(error.error),
      });
    },
  });

  return (
    <form className="space-y-4" onSubmit={handleSubmit(execute)}>
      <Input
        type="text"
        name="name"
        label="Name"
        registerProps={register("name")}
        error={errors.name}
      />
      <Input
        type="email"
        name="email"
        label="Email"
        registerProps={register("email")}
        error={errors.email}
      />
      <Input
        type="text"
        name="description"
        label="Description"
        registerProps={register("description")}
        error={errors.description}
      />
      <Button type="submit" loading={isExecuting}>
        Create Example
      </Butto
Read more
Ships withinbox-zero

The world's best AI personal assistant for email. Open source app to help you reach inbox zero fast.

Get the whole plugin
Stats
12,401
Stars
1,559
Forks
Active
Maintenance
TypeScript
Language
7h ago
Last commit
3y ago
Created
3h ago
Added

Repo: elie222/inbox-zero

Other skills on inbox-zero.