npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

routesync

v1.0.48

Published

Laravel routes to typed frontend SDKs.

Readme

RouteSync

Stop writing API clients by hand.

RouteSync syncs your Laravel (or PHP) routes to a fully-typed frontend SDK — complete with TypeScript types, a camelCase mapper, React/Vue Query hooks, and Next.js Server Actions. One command. Zero boilerplate.


Why

You've been there. The backend ships a new endpoint. You update the route, write a fetch wrapper, add the TypeScript type, hook it into React Query, map snake_case to camelCase, and fifteen minutes later you're still not done.

RouteSync does all of that. You point it at routes/api.php and it generates the whole thing.

# Step 1 — in your Laravel folder
npx routesync annotate --input routes/api.php  # auto-inject #[Response] to controllers
npx routesync scan --input routes/api.php --models

# Step 2 — in your frontend folder
npx routesync generate --manifest routesync.manifest.json --output src/api --next-actions --zod
✔ Annotated 12 method(s) across 6 controller file(s)

✔ Found 35 routes, 19 models → routesync.manifest.json

✔ SDK generated → src/api
  api.ts      Typed API client
  types.ts    TypeScript interfaces (from real DB columns)
  hooks.ts    React Query hooks
  actions.ts  Next.js Server Actions
  schemas.ts  Zod validation schemas
  index.ts    Barrel export

That's it. Your frontend has a typed client, real DB-derived types, Zod schemas, and ready-to-use hooks — and you didn't write any of it.


Packages

| Package | What it does | |---|---| | @routesync/sdk | The core developer API. defineApi, endpoint, resource, createService. | | @routesync/core | HTTP client engine, auth, path resolution, error handling. | | @routesync/cli | Scans routes + models, generates types + SDK + hooks + actions. | | @routesync/react | useApiQuery / useApiMutation hooks built on TanStack Query. | | @routesync/vue | Vue Query composables, same idea. |


Install

# SDK + React hooks
npm install routesync @tanstack/react-query

# Vue composables
npm install routesync @tanstack/vue-query

# With Zod validation
npm install routesync zod

Full Workflow (Laravel + Next.js)

1. Auto-annotate controllers (one-time setup)

Run this from your Laravel project root to auto-inject #[Response] attributes into every controller method:

npx routesync annotate --input routes/api.php

| Option | Description | |---|---| | --input <file> | Path to routes file (default: routes/api.php) | | --dry-run | Preview what would be injected without writing files | | --force | Re-annotate methods that already have #[Response] |

This command:

  • Detects return new XxxResource(...) / XxxResource::collection(...) / response()->json(new XxxResource(...)) in each controller method
  • Resolves the model from the Resource's @mixin docblock (or strips the Resource suffix as fallback)
  • Injects #[Response(Model::class)] or #[Response(Model::class, collection: true)] above the method
  • Adds use App\Attributes\Response; to controller imports automatically
  • Creates app/Attributes/Response.php if it doesn't exist yet

Tip: Preview first with --dry-run:

npx routesync annotate --input routes/api.php --dry-run

You only need to run this once, or again when you add new endpoints.

2. Scan routes & models

Run this from your Laravel project root:

npx routesync scan --input routes/api.php --models

| Option | Default | Description | |---|---|---| | --input | routes/api.php | Path to your Laravel routes file | | --output | routesync.manifest.json | Where to save the manifest | | --baseURL | http://localhost/api | API base URL | | --models | off | Also scan app/Models/ for real DB column types |

--models requirement: PHP must be available in your terminal and your database must be accessible (.env configured). The scanner runs a temporary PHP script via Laravel's bootstrap to read Schema::getColumns() from each Eloquent model.

Important — manifest location: The manifest is saved in whichever folder you run scan from. If you run it from your Laravel root, copy the manifest to your frontend folder before running generate:

# Windows PowerShell
copy ..\routesync.manifest.json .

# macOS / Linux
cp ../backend/routesync.manifest.json .

3. Generate the SDK

Run this from your frontend project root:

npx routesync generate --manifest routesync.manifest.json --output src/api --next-actions --zod

| Option | Default | Description | |---|---|---| | --manifest | routesync.manifest.json | Path to manifest from step 2 | | --output | src/api | Output folder | | --next-actions | off | Generate actions.ts (Next.js Server Actions) | | --zod | off | Generate schemas.ts (Zod validation) | | --no-hooks | off | Skip generating hooks.ts | | --msw | off | Generate MSW mock handlers |

Windows PowerShell note: Do not use backslash \ for line continuation. Run the command on a single line:

npx routesync generate --manifest routesync.manifest.json --output src/api --next-actions --zod

Generated files

src/api/
├── api.ts        ← defineApi() with all endpoints + Contract types
├── types.ts      ← TypeScript interfaces (real DB columns when --models used)
├── hooks.ts      ← useApiQuery / useApiMutation per endpoint
├── actions.ts    ← Next.js Server Actions (--next-actions)
├── schemas.ts    ← Zod schemas from FormRequest rules (--zod)
├── index.ts      ← Barrel re-export
└── core/
    └── models.ts ← Raw Eloquent model interfaces (when --models used)

4. Initialize the client

Call createClient once at app startup (e.g. in your layout or provider):

// src/lib/api-client.ts
import { createClient } from 'routesync'

createClient({
  baseURL: process.env.NEXT_PUBLIC_API_URL!, // e.g. http://localhost:8000/api
  withCredentials: true,
})

5. Use in components

import { useApiQuery, useApiMutation } from 'routesync/react'
import { api } from '@/api/api'

// GET — fetch data
function ProdukList() {
  const { data, isLoading } = useApiQuery(api.produk.get, {
    query: { page: 1, search: 'kaos' }
  })

  if (isLoading) return <p>Loading...</p>
  return <ul>{data?.map(p => <li key={p.id}>{p.nama}</li>)}</ul>
}

// GET with path params
function ProdukDetail({ id }: { id: string }) {
  const { data } = useApiQuery(api.produk.getId, { params: { id } })
  return <div>{data?.nama}</div>
}

// POST / mutation
function AddToCart({ produkItemId }: { produkItemId: string }) {
  const mutation = useApiMutation(api.cart.postItems)

  return (
    <button onClick={() => mutation.mutate({ body: { produk_item_id: produkItemId, qty: 1 } })}>
      Tambah ke Keranjang
    </button>
  )
}

6. Use Server Actions (Next.js)

import { produkGetAction, cartPostItemsAction } from '@/api/actions'

// GET — no params needed
const result = await produkGetAction({ query: { page: 1 } })
if (result.success) console.log(result.data)

// POST — with body
const result = await cartPostItemsAction({ body: { produk_item_id: '5', qty: 1 } })

// GET with path params — params are required
const result = await produkGetIdAction({ params: { id: '42' } })

Response Type Inference

RouteSync automatically infers the TypeScript response type for each endpoint. The scanner works through 7 stages in order, stopping at the first successful match.

How inference works

Controller method
      │
      ▼
Stage 1: PHP 8 #[RouteSyncResponse] attribute on method  ← most explicit
      │
      ▼
Stage 2: return new UserResource($user) in method body
      │  ├─ Stage 2a: #[RouteSyncResponse] on Resource class
      │  ├─ Stage 2b: @mixin \App\Models\User docblock
      │  ├─ Stage 2c: __construct(User $user) type hint
      │  ├─ Stage 2d: @var User $resource docblock
      │  ├─ Stage 2e: Strip "Resource" suffix → App\Models\*
      │  └─ Stage 2f: toArray() keys vs DB column matching
      │
      ▼
Stage 3: response()->json([...]) inline array
         keys matched against DB columns (min score 2)
      │
      ▼
  response: unknown  ← annotate manually if all stages fail

Zero-config inference (no annotation needed)

For the common convention UserResourceUser, RouteSync infers automatically:

// ✅ Auto-detected — no annotation needed
public function show(User $user): JsonResponse
{
    return new UserResource($user);
}

// ✅ Auto-detected — UserResource::collection → User[]
public function index(): JsonResponse
{
    return UserResource::collection(User::all());
}

Auto-annotate with CLI (recommended)

Instead of adding #[Response] by hand, let the CLI do it:

# Preview first
npx routesync annotate --input routes/api.php --dry-run

# Apply
npx routesync annotate --input routes/api.php

This scans every controller method, detects which Resource it returns, resolves the model, and injects #[Response(Model::class)] automatically. See Auto-annotate controllers for details.

Manual annotation with PHP 8 Attribute

Use #[RouteSyncResponse] when auto-inference fails — for example when the Resource name doesn't match the model, or the response is a DTO/custom shape.

Step 1 — Create the attribute class (app/Attributes/RouteSyncResponse.php):

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class RouteSyncResponse
{
    public function __construct(
        public readonly string $model,
        public readonly bool $collection = false,
    ) {}
}

Step 2 — Annotate your controller method:

use App\Attributes\RouteSyncResponse;
use App\Models\User;

class AuthController extends Controller
{
    #[RouteSyncResponse(model: User::class)]
    public function register(RegisterRequest $request): JsonResponse
    {
        $user = User::create($request->validated());
        $token = $user->createToken('auth')->plainTextToken;
        return response()->json(['token' => $token, 'user' => new UserResource($user)]);
    }

    #[RouteSyncResponse(model: User::class, collection: true)]
    public function index(): JsonResponse
    {
        return response()->json(User::all());
    }
}

Or annotate the Resource class directly (applies to all endpoints that return this Resource):

use App\Attributes\RouteSyncResponse;
use App\Models\User;

#[RouteSyncResponse(model: User::class)]
class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return ['id' => $this->id, 'name' => $this->name, 'email' => $this->email];
    }
}

Priority: annotation on controller method > annotation on Resource class > auto-inference.

When to annotate manually

| Situation | Solution | |---|---| | Resource name doesn't match model (PublicProfileResourceUser) | #[RouteSyncResponse(model: User::class)] | | Response is a DTO, not an Eloquent model | Add model manually after generate | | Controller returns response()->json([...]) without a Resource | #[RouteSyncResponse(model: User::class)] | | Multiple models in one response | Add model manually after generate | | Response is still unknown after scan | Add #[RouteSyncResponse] attribute |


Data Transformation

RouteSync handles all data mapping automatically:

| Direction | What happens | Where | |---|---|---| | Response (backend → frontend) | snake_casecamelCase keys | HttpClient interceptor | | Request (frontend → backend) | camelCasesnake_case keys | HttpClient interceptor | | Response unwrap | { data: T, message, meta }T | HttpClient .get() / .post() etc. | | Zod validation | Parse + validate response shape | Per-endpoint responseSchema |

No extra config needed. product_name from Laravel becomes productName in your component automatically.


Manual Route Definitions

If you don't have a Laravel backend, define routes manually:

import { defineApi, endpoint, resource } from 'routesync'

createClient({ baseURL: 'https://api.myapp.com/api' })

export const api = defineApi({
  auth: {
    login:  endpoint<{ token: string }>({ method: 'POST', path: '/login' }),
    logout: endpoint({ method: 'POST', path: '/logout', auth: true }),
  },
  produk: {
    list:   endpoint<ProdukItem[]>({ method: 'GET', path: '/produk' }),
    detail: endpoint<ProdukItem, { id: string }>({ method: 'GET', path: '/produk/:id' }),
    create: endpoint<ProdukItem, unknown, CreateProdukBody>({
      method: 'POST', path: '/produk', auth: true
    }),
  },
  cart: resource({
    auth: true,
    endpoints: {
      get:    { method: 'GET',    path: '/cart' },
      add:    { method: 'POST',   path: '/cart/items' },
      update: { method: 'PATCH',  path: '/cart/items/:id' },
      remove: { method: 'DELETE', path: '/cart/items/:id' },
    }
  })
})

endpoint<TResponse, TParams, TBody> — generic order:

  1. TResponse — shape returned by the backend
  2. TParams — path params like { id: string }
  3. TBody — POST/PUT/PATCH body shape

Authentication

import { createClient } from 'routesync'

const client = createClient({ baseURL: 'https://api.myapp.com/api' })

// After login — set token
client.setToken(response.token)

// On logout — clear token
client.removeToken()

Any endpoint with auth: true automatically gets Authorization: Bearer TOKEN injected. For Next.js Server Actions, the generated actions.ts reads the token from cookies automatically via getAuthHeaders().


React Query Hooks

RouteSync generates a highly organized, declarative hook system built on top of TanStack Query. Instead of writing wrapper components or calling useQuery manually, RouteSync generates a centralized hook registry configured via a declarative Domain Specific Language (DSL).

Auto-generated hooks.ts Structure

When you run npx routesync generate, RouteSync outputs src/api/hooks.ts containing:

import { defineHooks } from 'routesync/react'
import { api } from './api'
import { QueryKey } from './query-key'
import type {
  ProdukItemResourceIndex,
  ProdukItemResourceShow,
  AdminProdukForm,
} from './types'

export const typeOf = <T>() => ({} as T)

export const hooks = defineHooks({
  produk: {
    // 1. Compile-Time Resource Schema Metadata (Type Registry)
    types: {
      list: typeOf<ProdukItemResourceIndex>(),
      detail: typeOf<ProdukItemResourceShow>(),
      create: typeOf<never>(),
      update: typeOf<never>(),
    },
    queryKey: QueryKey.produk,
    endpoint: api.produk,
  },
  adminProduk: {
    types: {
      list: typeOf<never>(),
      detail: typeOf<never>(),
      create: typeOf<AdminProdukForm['Create']>(),
      update: typeOf<never>(),
    },
    queryKey: QueryKey.adminProduk,
    endpoint: api.adminProduk,
  }
})

// Unified hook exports per domain resource group
export const useProduk = hooks.produk
export const useAdminProduk = hooks.adminProduk

Using Hooks in Components

For standard REST/CRUD actions, you call the unified resource hooks directly. All payload and return types are fully inferred from the metadata registry:

import { useProduk } from '@/api/hooks'

function ProductCatalog() {
  // 1. GET (Index) — List all products (inferred as ProdukItemResourceIndex)
  const { data: products, isLoading } = useProduk.index()

  // 2. GET (Show) — View specific product details (inferred as ProdukItemResourceShow)
  const { data: detail } = useProduk.show(42)

  if (isLoading) return <p>Loading...</p>

  return (
    <div>
      <h1>{detail?.nama}</h1>
      <ul>
        {products?.map(p => <li key={p.id}>{p.nama}</li>)}
      </ul>
    </div>
  )
}

For actions that require mutations, call the hook and use the mutation helpers:

import { useAdminProduk } from '@/api/hooks'

function CreateProductForm() {
  const createMutation = useAdminProduk.create()

  const handleSubmit = (formData: any) => {
    // Payload type (AdminProdukForm['Create']) is automatically enforced here
    createMutation.mutate(formData, {
      onSuccess: () => console.log('Product created!')
    })
  }

  return <button onClick={() => handleSubmit({ nama: 'Kaos', ... })}>Create</button>
}

Custom Non-CRUD Action Hooks

Endpoints that do not fit into the standard CRUD pattern (e.g. POST /login, PATCH /profile) are automatically exposed on the same resource hook namespace as custom hooks:

import { useLogin, useProfile } from '@/api/hooks'

// Login mutation (POST /login)
const login = useLogin.useCreate()
login.mutate({ email, password })

// Profile update (PATCH /profile)
const updateProfile = useProfile.usePatch()
updateProfile.mutate({ name, email })

Declarative Cache Invalidation

You can define custom, cross-resource query cache invalidation rules using the cache metadata property. This ensures that when a mutation succeeds, related queries are automatically refreshed:

export const hooks = defineHooks({
  orders: {
    types: {
      list: typeOf<OrderResourceIndex>(),
      detail: typeOf<OrderResourceShow>(),
      create: typeOf<OrderForm['Create']>(),
      update: typeOf<never>(),
    },
    queryKey: QueryKey.orders,
    endpoint: api.orders,

    // Cache metadata defines runtime invalidation strategies
    cache: {
      create: {
        invalidate: [
          QueryKey.orders.lists,  // Refreshes the orders history list
          QueryKey.cart.summary,  // Refreshes the shopping cart summary query
        ]
      }
    }
  }
})

Zod Schema Validation

When using --zod with routesync generate, schemas.ts is generated from your Laravel FormRequest rules.

Requirement: You must use Laravel FormRequest classes for rules to be detected:

// ✅ RouteSync will auto-generate Zod schema
public function store(StoreProductRequest $request) { ... }

// ❌ Rules will not be detected
public function store(Request $request) {
    $request->validate([...]);
}

CLI Reference

# Auto-inject #[Response] attributes into controller methods
npx routesync annotate --input routes/api.php

# Preview without writing files
npx routesync annotate --input routes/api.php --dry-run

# Re-annotate already-annotated methods
npx routesync annotate --input routes/api.php --force

# Scan Laravel routes only
npx routesync scan --input routes/api.php

# Scan routes + Eloquent models (recommended)
npx routesync scan --input routes/api.php --models

# Generate SDK from manifest
npx routesync generate --manifest routesync.manifest.json --output src/api

# Generate everything
npx routesync generate --manifest routesync.manifest.json --output src/api --next-actions --zod

# Watch mode — auto re-generates on route file change
npx routesync watch --input routes/api.php --output src/api

How It Works

routes/api.php + app/Models/
         │
         ▼
npx routesync annotate        ← inject #[Response] into controllers
         │
         ▼
npx routesync scan --models   ← read routes + DB columns → manifest
         │
         ▼
   routesync.manifest.json
         │
         ▼
npx routesync generate        ← generate SDK from manifest
         │
         ▼
   src/api/
    ├── api.ts       ← defineApi + endpoints + Contract types
    ├── types.ts     ← interfaces from DB columns
    ├── hooks.ts     ← TanStack Query hooks
    ├── actions.ts   ← Next.js Server Actions
    └── schemas.ts   ← Zod schemas
         │
         ▼
   React / Vue / Next.js

The CLI parses your route file via PHP reflection (using Laravel's own bootstrap), builds a language-agnostic manifest, then feeds it to independent generators. Each generator can be used standalone.


Requirements

  • Node.js >= 20
  • PHP available in PATH (for scan --models and annotate)
  • Laravel project with database accessible (for scan --models)

License

MIT