NeevJS Documentation
Plugin-driven, offline-first React framework for real-world business applications.
Version 1.0.1-beta
· Built by Rahul Raj Kushwaha
Introduction
NeevJS ("नींव" = Foundation) is a React framework designed specifically for business applications — CRMs, admin dashboards, POS systems, booking platforms, and internal tools.
Instead of fetch, axios, and separate state management, NeevJS gives you a single abstraction:
useModel(). It works like Laravel Eloquent, but in React.
NeevJS CLI
The fastest way to start a new NeevJS project is with the interactive CLI.
npx @neevjs/cli init my-app
The CLI will guide you through selecting a Project Mode and setting up your environment.
Installation
Client (React)
npm install @neevjs/client react
Server (Optional Node backend)
npm install @neevjs/server
Quick Start
// 1. Create a client
import { createClient, NeevProvider, useModel, AuthPlugin } from '@neevjs/client'
const client = createClient({ baseURL: 'https://api.yourapp.com/api' })
client.use(AuthPlugin)
// 2. Wrap your app
function App() {
return (
<NeevProvider client={client}>
<Dashboard />
</NeevProvider>
)
}
// 3. Use data anywhere
function Dashboard() {
const { data, loading, create, remove } = useModel('orders')
if (loading) return <p>Loading...</p>
return (
<div>
{data.map(order => (
<div key={order.id}>
{order.name}
<button onClick={() => remove(order.id)}>Delete</button>
</div>
))}
<button onClick={() => create({ name: 'New Order' })}>Add</button>
</div>
)
}
API Contract
Every backend that works with NeevJS must follow this response format:
// Success response
{
"data": [], // array or object
"meta": {
"pagination": {
"page": 1,
"perPage": 20,
"total": 100,
"lastPage": 5
}
},
"error": null
}
// Error response
{
"data": null,
"meta": {},
"error": "Descriptive error message"
}
createClient
The core engine. Creates a client instance that all hooks and components use.
import { createClient } from '@neevjs/client'
const client = createClient({
baseURL: 'http://localhost:3001/api' // optional, defaults to ''
})
// Register plugins
client.use(AuthPlugin)
client.use(LoggerPlugin)
client.use(CachePlugin)
client.use(OfflinePlugin)
| Option | Type | Default | Description |
|---|---|---|---|
baseURL |
string | '' |
Base URL prepended to all API requests |
NeevProvider
Provides the client to the entire React tree via context. Wrap your root <App /> with it.
import { NeevProvider } from '@neevjs/client'
function App() {
return (
<NeevProvider client={client}>
{/* all components have access to client */}
<Router />
</NeevProvider>
)
}
useModel
The core hook. Replaces fetch, axios, useState, and useEffect for data management.
const {
data, // T[] — current records
loading, // boolean — fetching state
error, // Error|null — any error
create, // (payload) => Promise<void>
update, // (id, payload) => Promise<void>
remove, // (id) => Promise<void>
refresh, // () => Promise<void> — manual re-fetch
mutate, // (data) => void — manual cache update
} = useModel<YourType>('model-name')
// With Query Parameters (isolated cache per param set)
const { data } = useModel<YourType>('users', {
params: { role: 'admin', page: 1 }
})
// With React Suspense (eliminates loading checks)
const { data } = useModel<YourType>('model-name', { suspense: true })
// Hybrid Mode (override backend for this model)
const { data } = useModel<YourType>('payments', {
baseURL: 'https://api.payments-service.com/api'
})
With TypeScript generics
interface Order {
id: number
customer: string
amount: number
status: 'pending' | 'completed'
}
const { data, create } = useModel<Order>('orders')
// data is typed as Order[]
// create expects Omit<Order, 'id'>
React Suspense Mode
Pass { suspense: true } to enable Suspense-based data fetching. Wrap the component in
<NeevBoundary /> to handle loading and error states declaratively — no
if (loading) needed.
function UsersContent() {
// Component "suspends" until data is ready
const { data, remove } = useModel<User>('users', { suspense: true })
return (
<ul>
{data.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => remove(user.id)}>Delete</button>
</li>
))}
</ul>
)
}
// Wrap with NeevBoundary — handles loading + errors
export function UsersPage() {
return (
<NeevBoundary loadingFallback={<p>Loading...</p>}>
<UsersContent />
</NeevBoundary>
)
}
Caching & Deduplication
Results are cached in memory by model name with a 60-second stale time. If 3 components mount
simultaneously and all call useModel('users'), only one network request is made —
all components share the result.
create(),
update(), or remove() in any component, every other component using the same model
re-renders automatically with fresh data.
Offline Behaviour
When navigator.onLine is false and cached data exists, useModel skips
the network request entirely and serves from cache. No errors, no failed requests in the console.
useAuth
Access the auth system anywhere in your app.
const { login, logout, user, isAuthenticated, register } = useAuth()
// Login
await login('email@example.com', 'password')
// Get current user
const currentUser = await user() // returns AuthUser | null
// Check auth state
if (isAuthenticated()) { ... }
// Logout
logout()
// Register
await register('email@example.com', 'password', 'Full Name')
useStore & SecureStore
Client-side global state management — NeevJS's built-in answer to Redux for most use cases.
Any component calling useStore('same-key') shares the same reactive state with zero configuration.
Built on React 18’s useSyncExternalStore for concurrent-mode safety.
useStore is a convenient zero-dependency option
for most business apps — use what fits your team best.
Basic Usage
import { useStore } from '@neevjs/client'
// Works exactly like useState, but shared across ALL components
const [count, setCount] = useStore('counter', 0)
// Functional updates work too
setCount(prev => prev + 1)
Shopping Cart Example
interface CartItem {
id: number
name: string
qty: number
price: number
}
// In any component — they all share the same cart state
const [cart, setCart] = useStore<CartItem[]>('cart', [])
// Add item
setCart(prev => [...prev, { id: 1, name: 'Widget', qty: 1, price: 99 }])
// Remove item
setCart(prev => prev.filter(item => item.id !== 1))
// Update quantity
setCart(prev =>
prev.map(item => item.id === 1 ? { ...item, qty: item.qty + 1 } : item)
)
With localStorage Persistence
Pass { persist: true } and the store value survives page reloads automatically. The key is saved
under neev_store_<key> in localStorage.
// Theme preference — survives refresh
const [theme, setTheme] = useStore('theme', 'dark', { persist: true })
// Cart — survives refresh
const [cart, setCart] = useStore<CartItem[]>('cart', [], { persist: true })
UI State Examples
// Sidebar open/close — shared between layout and toggle button
const [sidebarOpen, setSidebarOpen] = useStore('sidebar', true)
// Toast notification queue
const [toasts, setToasts] = useStore<string[]>('toasts', [])
const addToast = (msg: string) => setToasts(prev => [...prev, msg])
// Multi-step wizard position
const [step, setStep] = useStore('wizard-step', 1)
Imperative API (Outside React)
Use getStore and setStore from service layers, plugins, or event handlers outside of
React components.
import { getStore, setStore } from '@neevjs/client'
// Read the current value anywhere
const cart = getStore<CartItem[]>('cart')
// Set a value — all subscribed components re-render
setStore('toasts', ['Payment successful!'])
API Reference
| Export | Signature | Description |
|---|---|---|
useStore |
(key, initial, options?) => [value, setter] |
React hook. All components with same key share state. |
getStore |
(key) => T | undefined |
Read a store value imperatively (outside React). |
setStore |
(key, value) => void |
Set a store value imperatively. Notifies all subscribers. |
| Option | Type | Default | Description |
|---|---|---|---|
persist |
boolean | false |
Persist value to localStorage. Survives page reloads. |
session |
boolean | false |
Persist to sessionStorage. Survives refresh but cleared on tab close. |
ttl |
number | undefined |
Time-to-live in milliseconds. Only works with persist: true. |
session |
boolean | false |
Persist to sessionStorage. Survives refresh but cleared on tab close. |
ttl |
number | undefined |
Time-to-live in milliseconds. Only works with persist: true. |
useStore vs useModel — When to Use Which
| Scenario | Recommended |
|---|---|
| Fetching users, orders, products from your API | useModel |
| Shopping cart, wishlist | useStore (in-memory or persist) |
| Theme, language preference | useStore({ persist: true }) |
| Toast / notification queue | useStore (in-memory) |
| Sidebar open, modal visibility | useStore or local useState |
| Multi-step form draft | useStore({ session: true }) |
| OTP state, masked payment info during session | SecureStore |
| Tokens, auth data | useAuth() (handled by NeevJS internally) |
| Server filters / pagination | useModel + URL params |
The Three Storage Tiers
Choose based on the sensitivity and lifetime of your data:
| Tier | API | Survives Refresh? | XSS Safe? | Disk Safe? |
|---|---|---|---|---|
| In-Memory (default) | useStore('k', v) |
❌ | ✅ | ✅ |
| Session | useStore('k', v, { session: true }) |
✅ (tab only) | ❌ | ✅ |
| Persist | useStore('k', v, { persist: true }) |
✅ | ❌ | ❌ |
| Encrypted | SecureStore.set('k', v) |
❌ | ✅ | ✅ |
persist: true or session: true, your
data is stored in plain text in the browser storage. Avoid storing highly sensitive data like plain-text
passwords or secret keys in these tiers. Use SecureStore or in-memory state for sensitive info.
await SecureStore.set('k', v)Session Storage Tier { session: true }
Data survives page refresh but is cleared when the tab closes and is not shared across tabs. Safer than localStorage for semi-sensitive data.
// Unsaved form draft
const [draft, setDraft] = useStore('invoice_draft', {}, { session: true })
// Current user object for the session
const [currentUser, setCurrentUser] = useStore('session_user', null, { session: true })
Persist Tier { persist: true, ttl? }
const [theme, setTheme] = useStore('theme', 'dark', { persist: true })
// Cart with 7-day expiry
const [cart, setCart] = useStore<CartItem[]>('cart', [], {
persist: true,
ttl: 7 * 24 * 60 * 60 * 1000,
})
SecureStore — AES-256-GCM Encrypted
For sensitive in-session data. Uses the Web Crypto API with a non-extractable session key —
the raw key bytes can never be exported even by JavaScript. The ciphertext in localStorage is unreadable
after a page refresh (key is gone), which is intentional.
import { SecureStore } from '@neevjs/client'
await SecureStore.set('pending_payment', { amount: 4999, currency: 'INR' })
const payment = await SecureStore.get<Payment>('pending_payment') // null after refresh
SecureStore.remove('pending_payment')
SecureStore.clear() // call on logout
Logout Cleanup
import { SecureStore, clearPersistedStore } from '@neevjs/client'
function handleLogout() {
auth.logout()
SecureStore.clear() // Wipe all encrypted session data
clearPersistedStore('cart') // Wipe persisted cart
}
Full API Reference
| Export | Signature | Description |
|---|---|---|
useStore |
(key, init, opts?) → [T, setter] |
React hook. Shared across all components with same key. |
getStore |
(key) → T | undefined |
Read imperatively outside React. |
setStore |
(key, value) → void |
Set imperatively. Notifies all subscribers. |
clearPersistedStore |
(key) → void |
Remove from localStorage and sessionStorage. |
SecureStore.set |
(key, value) → Promise<void> |
Encrypt and store. |
SecureStore.get |
(key) → Promise<T | null> |
Decrypt. Returns null if key is gone or tampered. |
SecureStore.remove |
(key) → void |
Remove one encrypted entry. |
SecureStore.clear |
() → void |
Wipe all encrypted entries. Use on logout. |
useStore vs Redux vs Zustand
useStore is zero-dependency and covers most business app needs out of the box.
| Feature | useStore (NeevJS) | Redux Toolkit | Zustand |
|---|---|---|---|
| Zero dependencies | ✅ | ❌ (redux, immer, reselect) | ✅ |
| Setup boilerplate | None | store + slice + Provider | create() call |
| API style | Identical to useState |
actions + dispatch | Custom (get/set) |
| sessionStorage tier built-in | ✅ | ❌ | ❌ (needs middleware) |
| localStorage + TTL built-in | ✅ | ❌ (needs redux-persist) | ❌ (needs middleware) |
| AES-256-GCM encrypted storage | ✅ SecureStore | ❌ | ❌ |
| Concurrent Mode safe | ✅ useSyncExternalStore | ✅ | ✅ |
| Fine-grained selectors | Planned v0.2 | ✅ useSelector | ✅ built-in |
| Time-travel DevTools | Planned v0.2 | ✅ Redux DevTools | ✅ |
| Best for | Business apps, no config | Large teams, complex flows | Medium apps |
When to choose Redux or Zustand instead
Prefer an external library when you need:
- Fine-grained selectors — Redux’s
useSelectorand Zustand’s selectors prevent re-renders when unrelated state changes. Critical for very large stores. - Time-travel debugging — Redux DevTools lets you replay state changes step-by-step.
- Complex derived/computed state — Reselect or Zustand’s computed values handle memoized derivations elegantly.
- Team familiarity — If your team already knows Redux well, keep using it.
Using Redux or Zustand alongside NeevJS
// ✅ Redux + NeevJS — no conflict
import { useSelector } from 'react-redux'
import { useModel } from '@neevjs/client'
function OrdersPage() {
const { data: orders } = useModel('orders') // server data
const filters = useSelector(state => state.ui.filters) // UI state
return <Table model="orders" />
}
// ✅ Zustand + NeevJS — no conflict
import { create } from 'zustand'
const useFilterStore = create(set => ({
status: 'all',
setStatus: (status) => set({ status }),
}))
function OrdersPage() {
const { data: orders } = useModel('orders')
const { status, setStatus } = useFilterStore()
// ...
}
useSyncStatus
Monitor offline/online status and pending sync queue. Used with OfflinePlugin.
const { isOffline, pending, syncing, errors, clearErrors } = useSyncStatus()
function SyncIndicator() {
const { isOffline, pending, syncing, errors, clearErrors } = useSyncStatus()
if (errors.length > 0) return (
<div style={{ color: 'red' }}>
⚠️ {errors.length} action(s) failed permanently.
<button onClick={clearErrors}>Dismiss</button>
</div>
)
if (syncing) return <span>↻ Syncing...</span>
if (isOffline) return <span>● Offline — {pending} queued</span>
return <span>● Online</span>
}
| Value | Type | Description |
|---|---|---|
isOffline |
boolean | true when navigator.onLine is false |
pending |
number | Number of actions in the offline queue |
syncing |
boolean | true while background sync is in progress |
errors |
Error[] | Permanently failed actions (e.g. server returned 4xx) |
clearErrors |
() => void | Clears the error list |
Table
Auto-renders a table for any model. Columns are auto-detected from data or can be customized.
// Auto columns — detected from first row
<Table model="users" />
// Custom columns with render functions
<Table<User>
model="users"
columns={[
{ key: 'name', label: 'Full Name' },
{ key: 'email', label: 'Email' },
{
key: 'role',
label: 'Role',
render: (val) => <Badge>{val}</Badge>
},
]}
onEdit={(user) => setEditUser(user)}
onDelete={(user) => remove(user.id)}
emptyMessage="No records yet."
/>
| Prop | Type | Description |
|---|---|---|
model |
string | Model name (matches useModel key) |
columns |
TableColumn[] | Optional. Auto-detected from data if omitted. |
onEdit |
(row: T) => void | Edit button click handler |
onDelete |
(row: T) => void | Delete button click handler |
emptyMessage |
string | Message shown when there is no data |
classNames |
TableClassNames | Override CSS class names for any element |
styles |
TableStyles | Override inline styles for any element |
unstyled |
boolean | Strip all default styles for full custom control |
Form
Model-aware form component that handles create and update in one component.
// Create new record
<Form<User>
model="users"
fields={[
{ name: 'name', label: 'Full Name', required: true },
{ name: 'email', type: 'email', required: true },
{
name: 'role',
type: 'select',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
]
},
]}
onSuccess={() => console.log('Created!')}
submitLabel="Create User"
/>
// Update existing record — pass editId + initialValues
<Form<User>
model="users"
editId={selectedUser.id}
initialValues={selectedUser}
fields={fields}
onSuccess={() => setEditing(false)}
submitLabel="Update User"
/>
transformPayload — Modify data before sending
<Form<User>
model="users"
transformPayload={(payload) => ({
...payload,
name: payload.name?.trim().toUpperCase(),
createdAt: new Date().toISOString(),
})}
fields={fields}
/>
onSubmitOverride — Custom submission logic
<Form<User>
model="users"
onSubmitOverride={async (payload) => {
await myCustomApiCall(payload)
await sendWelcomeEmail(payload.email)
}}
fields={fields}
/>
Field types
| type | Renders |
|---|---|
text (default) |
<input type="text"> |
email |
<input type="email"> |
password |
<input type="password"> |
number |
<input type="number"> |
textarea |
<textarea> |
select |
<select> with options |
All Form Props
| Prop | Type | Description |
|---|---|---|
model |
string | Model name for POST/PUT requests |
fields |
FormField[] | Schema-driven field definitions |
editId |
number | string | If set, sends PUT instead of POST |
initialValues |
Partial<T> | Pre-fills fields for edit mode |
onSuccess |
() => void | Called after successful submission |
onError |
(err: Error) => void | Called when submission fails |
submitLabel |
string | Text on the submit button |
transformPayload |
(payload) => payload | Modify data before sending to server |
onSubmitOverride |
(payload) => Promise<void> | Fully replace default submit logic |
children |
ReactNode | Add custom JSX fields inside the form |
classNames |
FormClassNames | Override CSS class names |
styles |
FormStyles | Override inline styles |
unstyled |
boolean | Strip all default styles |
fieldErrors |
Record<string, string> | External/server errors to display per-field |
validate |
(values) => Record<string, string> | Custom form-level validation logic |
NeevBoundary
Replaces all if (loading) and if (error) checks when using useModel with
{ suspense: true }. It combines React Suspense with an ErrorBoundary in one component.
import { NeevBoundary } from '@neevjs/client'
<NeevBoundary
loadingFallback={<SkeletonLoader />}
errorFallback={(error, reset) => (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
<UsersContent />
</NeevBoundary>
| Prop | Type | Description |
|---|---|---|
loadingFallback |
ReactNode | Shown while data is loading (Suspense fallback) |
errorFallback |
(error, reset) => ReactNode | Shown on error; call reset() to retry |
errorFallback is not provided, NeevBoundary renders a default "Data
Error" UI with a "Try again" button.Protected
Wraps content that requires authentication. Optionally checks user role.
// Require login only
<Protected>
<Dashboard />
</Protected>
// Require specific role
<Protected role="admin" fallback={<p>Admins only</p>}>
<AdminPanel />
</Protected>
AuthPlugin
Automatically injects the JWT token as an Authorization: Bearer ... header in every request.
import { AuthPlugin } from '@neevjs/client'
client.use(AuthPlugin)
// Now every request automatically includes the token.
// No manual header management needed.
LoggerPlugin
Logs all requests and responses to the browser console. Useful for development.
import { LoggerPlugin } from '@neevjs/client'
client.use(LoggerPlugin)
// Console output:
// [NeevJS] → GET /users
// [NeevJS] ← 200 http://localhost:3001/api/users
CachePlugin
Caches GET requests at the network level. Automatically invalidates on mutations (POST, PUT, DELETE).
import { CachePlugin, createCachePlugin } from '@neevjs/client'
// Default (1 minute TTL)
client.use(CachePlugin)
// Custom TTL
client.use(createCachePlugin({ ttl: 30_000 })) // 30 seconds
OfflinePlugin
Queues write operations (POST, PUT, DELETE) when the device is offline, then automatically retries when connectivity is restored.
import { OfflinePlugin } from '@neevjs/client'
client.use(OfflinePlugin)
// That's it. useModel().create/update/remove
// will queue automatically when offline.
// Show status to user
const { isOffline, pending } = useSyncStatus()
{isOffline && <p>Offline — {pending} actions pending</p>}
Custom Plugin
Plugins follow a simple interface. Any of the hooks are optional.
import type { NeevPlugin } from '@neevjs/client'
const TenantPlugin: NeevPlugin = {
name: 'tenant',
onRequest(req) {
return {
...req,
options: {
...req.options,
headers: {
...req.options.headers,
'X-Tenant-ID': 'company-123',
},
},
}
},
onResponse(res) {
// transform or inspect response
return res
},
onError(err) {
// log to error tracking (Sentry, etc.)
console.error('API Error:', err)
},
}
client.use(TenantPlugin)
Feature-Driven Architecture
NeevJS recommends organizing projects by business feature, not by technical role. This prevents "Massive Component Syndrome" — 1000-line files that are impossible to debug.
Recommended Folder Structure
src/
├── app/ ← Global app shell
│ ├── App.tsx ← Layout, routing, providers
│ └── main.tsx ← React DOM entry point
│
├── core/ ← Framework configuration
│ └── neev.ts ← createClient + plugin registry
│
├── models/ ← Shared domain types
│ ├── user.ts ← Used by multiple features
│ └── order.ts
│
├── shared/ ← Generic reusable UI
│ └── components/
│ └── SyncIndicator.tsx
│
└── features/ ← One folder per domain
├── users/
│ ├── components/
│ │ ├── UserTable.tsx ← Atomic sub-components
│ │ └── UserForm.tsx
│ └── UsersPage.tsx ← The thin orchestrator
├── dashboard/
│ └── DashboardPage.tsx
└── auth/
└── LoginPage.tsx
The Orchestrator Pattern
Keep each "Page" file as a thin orchestrator. It manages only UI state (toggling modals, passing handlers). Actual UI lives in small, atomic sub-components.
// features/users/UsersPage.tsx ← only ~50 lines, clean and readable
function UsersContent() {
const { remove } = useModel<User>('users', { suspense: true })
const [showForm, setShowForm] = useState(false)
const [editUser, setEditUser] = useState<User | null>(null)
return (
<div>
<button onClick={() => setShowForm(true)}>+ Add User</button>
{showForm && (
<UserForm
editUser={editUser}
onSuccess={() => { setShowForm(false); setEditUser(null) }}
/>
)}
<UserTable
onEdit={(user) => { setEditUser(user); setShowForm(true) }}
onDelete={(user) => remove(user.id)}
/>
</div>
)
}
User type is used by the
Users feature, Dashboard stats, and the Auth system. Keeping it in src/models/ instead of inside
any one feature prevents duplication and circular imports.React 19 Compiler
NeevJS is built for React 19 and ships with the React Compiler pre-configured
in the demo app. This means you never need to write useMemo,
useCallback, or React.memo again.
What the Compiler Does Automatically
// ❌ Before (manual memoization)
const handleDelete = useCallback((id) => {
remove(id)
}, [remove])
const expensiveList = useMemo(() => {
return data.filter(u => u.role === 'admin')
}, [data])
// ✅ After (with React Compiler — just write normal code)
const handleDelete = (id) => remove(id)
const expensiveList = data.filter(u => u.role === 'admin')
Enabling in Your Vite Project
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
})
"react": "^19.0.0" in package.json.@neevjs/server
An optional Node.js + Express backend that follows the NeevJS API contract out of the box. It includes auth routes, a generic CRUD controller, and an in-memory DB (swap for PostgreSQL in production).
# Install dependencies
cd packages/server
npm install
# Start dev server
npm run dev
# → Running at http://localhost:3001
# Available routes:
GET /api/health
POST /api/auth/login
POST /api/auth/register
GET /api/auth/me (protected)
POST /api/auth/logout (protected)
GET /api/users
POST /api/users
GET /api/users/:id
PUT /api/users/:id
DELETE /api/users/:id
Add a new model
In server.ts, add one line:
// Unprotected
app.use('/api/orders', createModelRouter('orders'))
// Protected (requires auth token)
app.use('/api/invoices', createModelRouter('invoices', true))
Seed data
In src/db/index.ts:
db.seed('orders', [
{ id: 1, customer: 'Rahul', amount: 5000, status: 'pending' },
])
Project Modes
NeevJS is designed to adapt to your architecture. Choose the mode that fits your team.
1. Fullstack Mode (Default)
The best developer experience. Uses @neevjs/server (Node.js/Express) as the backend. Everything is
pre-configured with shared types and unified validation.
2. API Mode
Use NeevJS as a powerful client for your existing backend. Whether you use Laravel, Django, Rails, or a custom Go API, NeevJS works as long as you follow the API Contract.
const client = createClient({
baseURL: 'https://api.your-company.com/v1'
})
3. Hybrid Mode
The most flexible approach for microservices or legacy migration. Most models use your primary backend, but specific models connect directly to external or legacy services.
// core/neev.ts
const client = createClient({ baseURL: '/api' }) // Primary
// features/payments/PaymentsPage.tsx
const { data } = useModel('payments', {
baseURL: 'https://api.external-processor.com/v1' // External
})
Contributing
Contributions are highly welcome! To contribute to NeevJS, follow these steps:
- Fork the repository on GitHub.
- Create your feature branch:
git checkout -b feature/MyFeature - Commit your changes:
git commit -m 'feat: add MyFeature' - Push to the branch:
git push origin feature/MyFeature - Open a Pull Request.
Please ensure your code adheres to the existing style and all tests pass before submitting your PR.