Skip to content

carlonicora/nextjs-jsonapi

Repository files navigation

@carlonicora/nextjs-jsonapi

A comprehensive Next.js package providing JSON:API compliant client with unified server/client support, automatic caching, and a complete shadcn/ui component library.

Table of Contents

Features

  • Unified API: Auto-detects environment (server/client) and uses the appropriate request method
  • JSON:API Compliance: Full JSON:API specification support with deserialization and pagination
  • Next.js 16+ Caching: Built-in support for cacheLife() and cacheTag() via cache profiles
  • React Hooks: useJsonApiGet and useJsonApiMutation for client-side data fetching
  • Server Components: Direct server-side data fetching with automatic token handling
  • Multi-Tenant Support: Built-in company ID handling for B2B applications
  • File Uploads: Seamless file upload support with multipart requests
  • shadcn/ui Components: 44 pre-built UI components (41 standard + 3 custom)
  • Utility Functions: cn class merger, mobile detection, and ref composition

Architecture

The library is organized into eight entry points:

@carlonicora/nextjs-jsonapi
├── (main)          # Unified API (auto-detects environment)
├── /core           # Interfaces, factories, registries, and utilities
├── /client         # React hooks and client-side utilities
├── /server         # Server-side requests and caching
├── /permissions    # Permission checking utilities
├── /features       # Built-in feature modules (S3, etc.)
├── /utils          # Utility functions (cn, useIsMobile, etc.)
└── /shadcnui       # 44 shadcn/ui components

Installation

pnpm add @carlonicora/nextjs-jsonapi

Git Submodule Setup (Alternative)

If you want to use the package as a git submodule (for development or before npm release):

1. Add the submodule

cd /path/to/your-project
git submodule add https://github.com/carlonicora/nextjs-jsonapi packages/nextjs-jsonapi

2. Verify it worked

git submodule status
# Should show: <commit-sha> packages/nextjs-jsonapi (heads/master)

3. Commit the submodule

git add .gitmodules packages/nextjs-jsonapi
git commit -m "Add nextjs-jsonapi as submodule"

4. Update your package.json (e.g., apps/web/package.json)

{
  "dependencies": {
    "@carlonicora/nextjs-jsonapi": "workspace:*"
  }
}

5. Ensure pnpm-workspace.yaml includes packages

packages:
  - "apps/*"
  - "packages/*"

6. Install and build

pnpm install
cd packages/nextjs-jsonapi && pnpm build && cd ../..

For CI/CD (GitHub Actions), add submodules: recursive to your checkout step:

- uses: actions/checkout@v4
  with:
    submodules: recursive

Cloning a project with submodules:

# When cloning fresh
git clone --recurse-submodules https://github.com/your/repo.git

# If already cloned
git submodule update --init --recursive

Peer Dependencies

Package Version Required Purpose
next >=14.0.0 Yes Next.js framework
react >=18.0.0 Yes React library
react-dom >=18.0.0 Yes React DOM
react-hook-form >=7.0.0 Optional Form handling (for form components)

Quick Start

1. Configure the API Client

Configure the JSON:API client in your environment setup file:

// src/config/env.ts
import { configureJsonApi } from "@carlonicora/nextjs-jsonapi";
import { bootstrap } from "@/config/Bootstrapper";

configureJsonApi({
  apiUrl: process.env.NEXT_PUBLIC_API_URL!,
  bootstrapper: bootstrap,
});

2. Define Your Modules and Bootstrapper

Create a bootstrapper that registers all modules for both the ModuleRegistry (for Modules.X access) and DataClassRegistry (for JSON:API response translation):

// src/config/Bootstrapper.ts
import { DataClassRegistry, FieldSelector, ModuleRegistry } from "@carlonicora/nextjs-jsonapi/core";
import { ModuleWithPermissions } from "@carlonicora/nextjs-jsonapi/permissions";
import { S3Module } from "@carlonicora/nextjs-jsonapi/features";

// Import your module definitions
import { ArticleModule } from "@/features/article/ArticleModule";
import { UserModule } from "@/features/user/UserModule";
import { Article } from "@/features/article/data/Article";
import { User } from "@/features/user/data/User";

// Module factory helper
const moduleFactory = (params: {
  pageUrl?: string;
  name: string;
  cache?: string;
  model: any;
  feature?: string;
  moduleId?: string;
  inclusions?: Record<string, { types?: string[]; fields?: FieldSelector<any>[] }>;
}): ModuleWithPermissions => ({
  pageUrl: params.pageUrl,
  name: params.name,
  model: params.model,
  feature: params.feature,
  moduleId: params.moduleId,
  cache: params.cache,
  inclusions: params.inclusions ?? {},
});

// Example module definition file (e.g., ArticleModule.ts)
// export const ArticleModule = (factory: ModuleFactory) =>
//   factory({ name: "articles", model: Article, pageUrl: "/articles" });

// Single source of truth for all modules
const allModules = {
  Article: ArticleModule(moduleFactory),
  User: UserModule(moduleFactory),
  S3: S3Module(moduleFactory), // Built-in S3 module from library
} satisfies Record<string, ModuleWithPermissions>;

// Export type for TypeScript autocompletion
export type AllModuleDefinitions = typeof allModules;

let bootstrapped = false;

export function bootstrap(): void {
  if (bootstrapped) return;

  // Register modules for Modules.X access
  Object.entries(allModules).forEach(([name, module]) => {
    ModuleRegistry.register(name, module);
  });

  // Register model classes for JSON:API response translation
  DataClassRegistry.bootstrap(allModules);

  bootstrapped = true;
}

3. Fetch Data in Server Components

// src/app/articles/page.tsx
import { JsonApiGet } from "@carlonicora/nextjs-jsonapi";
import { Modules } from "@carlonicora/nextjs-jsonapi/core";

export default async function ArticlesPage() {
  const response = await JsonApiGet({
    classKey: Modules.Article,
    endpoint: "/articles",
    language: "en",
  });

  if (!response.ok) {
    return <div>Error: {response.error}</div>;
  }

  return (
    <ul>
      {response.data.map((article) => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  );
}

4. Use Hooks in Client Components

"use client";

import { useJsonApiGet, useJsonApiMutation } from "@carlonicora/nextjs-jsonapi/client";
import { Modules } from "@carlonicora/nextjs-jsonapi/core";

export function ArticleList() {
  const { data, loading, error, refetch } = useJsonApiGet({
    classKey: Modules.Article,
    endpoint: "/articles",
  });

  const { mutate, loading: creating } = useJsonApiMutation({
    method: "POST",
    classKey: Modules.Article,
    onSuccess: () => refetch(),
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <button
        onClick={() => mutate({
          endpoint: "/articles",
          body: { title: "New Article" }
        })}
        disabled={creating}
      >
        Create Article
      </button>
      <ul>
        {data.map((article) => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </div>
  );
}

Environment Variables

# Required
NEXT_PUBLIC_API_URL=http://localhost:3000

# Optional - Token cookie name (default: "token")
# Set this if your API uses a different cookie name for JWT tokens

Entry Points

Main Export (.)

The default export provides the unified API that auto-detects the environment:

import {
  JsonApiGet,
  JsonApiPost,
  JsonApiPut,
  JsonApiPatch,
  JsonApiDelete,
  configureJsonApi,
} from "@carlonicora/nextjs-jsonapi";

Core (/core)

Core interfaces, factories, registries, and utilities:

import {
  // Interfaces
  ApiDataInterface,
  ApiRequestDataTypeInterface,
  ApiResponseInterface,

  // Factories
  JsonApiDataFactory,

  // Registries
  ModuleRegistry, // Register modules during bootstrap
  DataClassRegistry, // Register model classes for JSON:API translation
  Modules, // Access registered modules (e.g., Modules.Article)

  // Endpoint builder
  EndpointBuilder,

  // Field selectors
  FieldSelector,

  // Utilities
  translateResponse,
} from "@carlonicora/nextjs-jsonapi/core";

Client (/client)

React hooks and client-side utilities (requires "use client"):

import {
  // Hooks
  useJsonApiGet,
  useJsonApiMutation,
  useRehydration,

  // Context
  JsonApiProvider,
  useJsonApiContext,

  // Request utilities
  directFetch,
  getClientToken,
} from "@carlonicora/nextjs-jsonapi/client";

Server (/server)

Server-side request utilities:

import { serverRequest, getServerToken, getCacheProfile } from "@carlonicora/nextjs-jsonapi/server";

Permissions (/permissions)

Permission checking utilities:

import {
  checkPermission,
  type PermissionCheck,
  type ModuleWithPermissions,
  type ModuleFactory,
} from "@carlonicora/nextjs-jsonapi/permissions";

Features (/features)

Built-in feature modules that can be used directly in your application:

import {
  // S3 Module (for file uploads via pre-signed URLs)
  S3Module, // Module definition factory
  S3Service, // Service with getPreSignedUrl, getSignedUrl, deleteFile
  S3, // Data class
  type S3Interface, // Response interface
  type S3Input, // Input parameters
} from "@carlonicora/nextjs-jsonapi/features";

// Usage example:
const s3Response = await S3Service.getPreSignedUrl({
  key: "companies/123/documents/file.pdf",
  contentType: "application/pdf",
  isPublic: true,
});

await fetch(s3Response.url, {
  method: "PUT",
  headers: s3Response.headers,
  body: file,
});

Utils (/utils)

Utility functions:

import {
  cn, // Class name merger (clsx + tailwind-merge)
  composeRefs, // Compose multiple refs
  useComposedRefs, // Hook for composing refs
  useIsMobile, // Mobile detection hook
  type ClassValue, // Type for cn function
} from "@carlonicora/nextjs-jsonapi/utils";

shadcn/ui (/shadcnui)

All shadcn/ui components (requires "use client"):

import {
  // UI Components (41)
  Accordion,
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
  Alert,
  AlertTitle,
  AlertDescription,
  AlertDialog,
  AlertDialogTrigger,
  AlertDialogContent /* ... */,
  Avatar,
  AvatarImage,
  AvatarFallback,
  Badge,
  badgeVariants,
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem /* ... */,
  Button,
  buttonVariants,
  Calendar,
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
  CardFooter,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  ChartLegend /* ... */,
  Checkbox,
  Collapsible,
  CollapsibleTrigger,
  CollapsibleContent,
  Command,
  CommandInput,
  CommandList,
  CommandItem /* ... */,
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent /* ... */,
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader /* ... */,
  Drawer,
  DrawerTrigger,
  DrawerContent /* ... */,
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent /* ... */,
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl /* ... */,
  HoverCard,
  HoverCardTrigger,
  HoverCardContent,
  Input,
  Label,
  NavigationMenu,
  NavigationMenuList,
  NavigationMenuItem /* ... */,
  Popover,
  PopoverTrigger,
  PopoverContent,
  Progress,
  RadioGroup,
  RadioGroupItem,
  ScrollArea,
  ScrollBar,
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem /* ... */,
  Separator,
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader /* ... */,
  Sidebar,
  SidebarProvider,
  SidebarContent,
  SidebarMenu /* ... */,
  Skeleton,
  Slider,
  Sonner,
  Toaster,
  toast,
  Switch,
  Table,
  TableHeader,
  TableBody,
  TableRow,
  TableHead,
  TableCell /* ... */,
  Tabs,
  TabsList,
  TabsTrigger,
  TabsContent,
  Textarea,
  Toggle,
  toggleVariants,
  Tooltip,
  TooltipTrigger,
  TooltipContent,
  TooltipProvider,

  // Custom Components (3)
  Kanban,
  KanbanColumn,
  KanbanItem, // Drag-and-drop Kanban board
  Link, // next-intl compatible Link
  MultiSelect, // Multi-select dropdown
} from "@carlonicora/nextjs-jsonapi/shadcnui";

Unified API

The unified API automatically detects whether code is running on the server or client and uses the appropriate request method.

JsonApiGet

Fetch data from a JSON:API endpoint:

const response = await JsonApiGet({
  classKey: Modules.Article, // Module definition
  endpoint: "/articles/123", // API endpoint
  companyId: "company-uuid", // Optional: for multi-tenant apps
  language: "en", // Required: for i18n
});

if (response.ok) {
  console.log(response.data); // Deserialized data
  console.log(response.pagination); // Pagination info

  // Navigate pages
  if (response.next) {
    const nextPage = await response.nextPage();
  }
}

JsonApiPost

Create a new resource:

const response = await JsonApiPost({
  classKey: Modules.Article,
  endpoint: "/articles",
  body: { title: "New Article", content: "..." },
  language: "en",

  // Optional
  files: { attachment: file }, // File uploads
  overridesJsonApiCreation: false, // Use raw body instead of JSON:API format
  responseType: Modules.OtherType, // If response type differs
});

JsonApiPut / JsonApiPatch

Update a resource:

const response = await JsonApiPut({
  classKey: Modules.Article,
  endpoint: "/articles/123",
  body: { title: "Updated Title" },
  language: "en",
});

JsonApiDelete

Delete a resource:

const response = await JsonApiDelete({
  classKey: Modules.Article,
  endpoint: "/articles/123",
  language: "en",
});

Client Hooks

useJsonApiGet

Hook for fetching data with automatic refetching:

const {
  data, // Fetched data or null
  loading, // Loading state
  error, // Error message or null
  response, // Full API response
  refetch, // Manual refetch function
  hasNextPage, // Pagination: has next page
  hasPreviousPage, // Pagination: has previous page
  fetchNextPage, // Fetch next page
  fetchPreviousPage, // Fetch previous page
} = useJsonApiGet<Article>({
  classKey: Modules.Article,
  endpoint: `/articles/${id}`,
  companyId: companyId,
  options: {
    enabled: !!id, // Conditionally enable
    deps: [someDependency], // Refetch when these change
  },
});

useJsonApiMutation

Hook for mutations (POST, PUT, PATCH, DELETE):

const {
  data, // Result data or null
  loading, // Loading state
  error, // Error message or null
  response, // Full API response
  mutate, // Execute the mutation
  reset, // Reset state
} = useJsonApiMutation<Article>({
  method: "POST",
  classKey: Modules.Article,
  onSuccess: (data) => console.log("Created:", data),
  onError: (error) => console.error("Failed:", error),
});

// Execute mutation
const result = await mutate({
  endpoint: "/articles",
  body: { title: "New Article" },
  files: { image: imageFile },
  companyId: "company-uuid",
});

Server Requests

For server components or API routes, use the server module directly:

import { serverRequest, getServerToken, getCacheProfile } from "@carlonicora/nextjs-jsonapi/server";

export async function getArticle(id: string) {
  const token = await getServerToken();

  const data = await serverRequest({
    method: "GET",
    url: `${process.env.NEXT_PUBLIC_API_URL}/articles/${id}`,
    token,
    cache: getCacheProfile("articles"), // Get cache settings
    language: "en",
  });

  return data;
}

Cache Profiles

The library supports Next.js 16+ caching via cacheLife() and cacheTag():

// In your module definition
export const Modules = {
  Article: {
    type: "articles",
    cache: "articles", // Profile name for caching
    factory: (data: any) => data,
  },
};

// The cache profile is automatically applied when using JsonApiGet
// on the server side

Permissions

Check user permissions for protected resources:

import { checkPermission } from "@carlonicora/nextjs-jsonapi/permissions";

// Check if user has permission
const canEdit = checkPermission({
  user: currentUser,
  action: "edit",
  resource: "articles",
  resourceId: article.id,
});

if (!canEdit) {
  return <div>Access denied</div>;
}

shadcn/ui Components

The package includes 44 pre-built shadcn/ui components:

Standard UI Components (41)

Component Description
Accordion Collapsible content sections
Alert Callout for important messages
AlertDialog Modal dialog for confirmations
Avatar User profile images
Badge Status indicators and labels
Breadcrumb Navigation breadcrumbs
Button Click actions with variants
Calendar Date picker calendar
Card Content container
Carousel Sliding content panels
Chart Data visualization (Recharts)
Checkbox Toggle options
Collapsible Expandable sections
Command Command palette (cmdk)
ContextMenu Right-click menus
Dialog Modal windows
Drawer Sliding side panels (Vaul)
DropdownMenu Dropdown menus
Form Form handling (react-hook-form)
HoverCard Hover-triggered cards
Input Text input fields
Label Form labels
NavigationMenu Navigation menus
Popover Floating content
Progress Progress indicators
RadioGroup Radio button groups
ScrollArea Custom scrollbars
Select Dropdown selects
Separator Visual dividers
Sheet Side panels
Sidebar Application sidebars
Skeleton Loading placeholders
Slider Range sliders
Sonner Toast notifications
Switch Toggle switches
Table Data tables
Tabs Tabbed interfaces
Textarea Multi-line text input
Toggle Toggle buttons
Tooltip Hover tooltips

Custom Components (3)

Component Description
Kanban Drag-and-drop Kanban board (dnd-kit)
Link next-intl compatible link wrapper
MultiSelect Multi-select dropdown with badges

Usage Example

"use client";

import {
  Button,
  Card,
  CardHeader,
  CardTitle,
  CardContent,
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@carlonicora/nextjs-jsonapi/shadcnui";
import { cn } from "@carlonicora/nextjs-jsonapi/utils";

export function ArticleCard({ article, className }) {
  return (
    <Card className={cn("hover:shadow-lg transition-shadow", className)}>
      <CardHeader>
        <CardTitle>{article.title}</CardTitle>
      </CardHeader>
      <CardContent>
        <p>{article.excerpt}</p>

        <Dialog>
          <DialogTrigger>
            <Button variant="outline">Read More</Button>
          </DialogTrigger>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>{article.title}</DialogTitle>
            </DialogHeader>
            <p>{article.content}</p>
          </DialogContent>
        </Dialog>
      </CardContent>
    </Card>
  );
}

Tailwind CSS Configuration

Important for Tailwind v4: You must add the @source directive to your globals.css to ensure Tailwind scans the package's component files:

/* apps/web/src/app/globals.css */
@import "tailwindcss";
@import "tw-animate-css";

/* Include package source files for Tailwind to scan */
@source "../../../../packages/nextjs-jsonapi/src/**/*.{ts,tsx}";

@custom-variant dark (&:is(.dark *));

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  /* ... other theme variables */
}

The @source path should be relative from your globals.css to the package's src directory.

CSS Variables

The shadcn/ui components require CSS variables to be defined in your application. Add these to your globals.css:

:root {
  /* Background & Foreground */
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);

  /* Primary */
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);

  /* Secondary */
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);

  /* Muted */
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);

  /* Accent */
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);

  /* Destructive */
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(0.985 0 0);

  /* Border & Input */
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);

  /* Card & Popover */
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);

  /* Charts */
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);

  /* Sidebar */
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);

  /* Warning */
  --warning: oklch(0.84 0.16 84);
  --warning-foreground: oklch(0.28 0.07 46);

  /* Radius */
  --radius: 0.625rem;
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.269 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  /* ... other dark mode values */
}

License

This project is licensed under GPL v3 for open source use.

For commercial/closed-source licensing, contact: @carlonicora

Author

Carlo Nicora - @carlonicora

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages