@btst/stack 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +790 -2
- package/dist/api/index.cjs +3 -2
- package/dist/api/index.d.cts +3 -3
- package/dist/api/index.d.mts +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.mjs +3 -2
- package/dist/client/index.cjs +3 -15
- package/dist/client/index.d.cts +4 -10
- package/dist/client/index.d.mts +4 -10
- package/dist/client/index.d.ts +4 -10
- package/dist/client/index.mjs +3 -10
- package/dist/context/index.cjs +14 -2
- package/dist/context/index.d.cts +6 -3
- package/dist/context/index.d.mts +6 -3
- package/dist/context/index.d.ts +6 -3
- package/dist/context/index.mjs +14 -3
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/plugins/index.cjs +1 -2
- package/dist/plugins/index.d.cts +3 -3
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.d.ts +3 -3
- package/dist/plugins/index.mjs +1 -1
- package/dist/shared/{stack.CwGEQ10b.mjs → stack.3OUyGp_E.mjs} +2 -8
- package/dist/shared/{stack.Br2KMECJ.cjs → stack.CktCg4PJ.cjs} +1 -8
- package/dist/shared/{stack.Dva9muUy.d.cts → stack.DORw_1ps.d.cts} +3 -25
- package/dist/shared/{stack.Dva9muUy.d.mts → stack.DORw_1ps.d.mts} +3 -25
- package/dist/shared/{stack.Dva9muUy.d.ts → stack.DORw_1ps.d.ts} +3 -25
- package/dist/shared/{stack.DvFqFlOV.d.ts → stack.DrUAVfIH.d.cts} +2 -7
- package/dist/shared/{stack.DvFqFlOV.d.cts → stack.DrUAVfIH.d.mts} +2 -7
- package/dist/shared/{stack.DvFqFlOV.d.mts → stack.DrUAVfIH.d.ts} +2 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,3 +1,791 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Better Stack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**A composable, plugin-based framework for building full-stack TypeScript applications**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@btst/stack)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Better Stack is a modern, type-safe framework for building full-stack applications with a plugin architecture. It seamlessly integrates:
|
|
15
|
+
|
|
16
|
+
- **🔌 Plugin Architecture** - Build modular, reusable features as standalone plugins
|
|
17
|
+
- **🔄 Full-Stack Type Safety** - End-to-end TypeScript with automatic type inference, no casts needed
|
|
18
|
+
- **🗄️ Schema-First Database** - Define your data models once using Better DB with typed adapters
|
|
19
|
+
- **🚀 Production Ready** - Built on proven libraries (Better Call, Better DB, Yar Router)
|
|
20
|
+
- **⚡ Zero Config** - Works out of the box with sensible defaults
|
|
21
|
+
- **✨ Developer Experience** - Helper functions preserve route keys, hook names, and endpoint types
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @btst/stack
|
|
27
|
+
# or
|
|
28
|
+
pnpm add @btst/stack
|
|
29
|
+
# or
|
|
30
|
+
yarn add @btst/stack
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Dependencies
|
|
34
|
+
|
|
35
|
+
Better Stack works with your existing stack:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @btst/db @btst/adapter-memory better-call @btst/yar zod
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### 1. Create Your Backend API
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// app/api/route.ts
|
|
47
|
+
import { betterStack } from "@btst/stack/api";
|
|
48
|
+
import { myPlugin } from "./plugins/my-plugin";
|
|
49
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
50
|
+
|
|
51
|
+
const api = betterStack({
|
|
52
|
+
plugins: {
|
|
53
|
+
myFeature: myPlugin.backend,
|
|
54
|
+
},
|
|
55
|
+
adapter: createMemoryAdapter,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Export for Next.js App Router
|
|
59
|
+
export const GET = api.handler;
|
|
60
|
+
export const POST = api.handler;
|
|
61
|
+
export const PUT = api.handler;
|
|
62
|
+
export const DELETE = api.handler;
|
|
63
|
+
export const PATCH = api.handler;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Create Your Client
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// app/lib/client.ts
|
|
70
|
+
import { createStackClient } from "@btst/stack/client";
|
|
71
|
+
import { myPlugin } from "./plugins/my-plugin";
|
|
72
|
+
|
|
73
|
+
export const client = createStackClient({
|
|
74
|
+
plugins: {
|
|
75
|
+
myFeature: myPlugin.client,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Access router and hooks
|
|
80
|
+
export const { router, hooks } = client;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Use in Your Components
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// app/components/MyComponent.tsx
|
|
87
|
+
"use client";
|
|
88
|
+
import { hooks } from "../lib/client";
|
|
89
|
+
|
|
90
|
+
export function MyComponent() {
|
|
91
|
+
const { useMyData } = hooks.myFeature;
|
|
92
|
+
const { data, isLoading } = useMyData();
|
|
93
|
+
|
|
94
|
+
if (isLoading) return <div>Loading...</div>;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
{data?.items.map((item) => (
|
|
99
|
+
<div key={item.id}>{item.name}</div>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Building Custom Plugins
|
|
107
|
+
|
|
108
|
+
Better Stack's power comes from its plugin architecture. Here's how to build your own:
|
|
109
|
+
|
|
110
|
+
### Plugin Architecture
|
|
111
|
+
|
|
112
|
+
Better Stack uses **separate backend and client plugins** to:
|
|
113
|
+
- ✅ Prevent SSR issues
|
|
114
|
+
- ✅ Enable better code splitting
|
|
115
|
+
- ✅ Allow independent deployment and versioning
|
|
116
|
+
- ✅ Improve tree-shaking
|
|
117
|
+
|
|
118
|
+
Each plugin type is completely independent:
|
|
119
|
+
|
|
120
|
+
1. **Backend Plugin** - API endpoints, database schema, and business logic
|
|
121
|
+
2. **Client Plugin** - Routes, components, and React hooks
|
|
122
|
+
|
|
123
|
+
### Example: Messages Plugin (Backend)
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// plugins/messages/backend.ts
|
|
127
|
+
import { createDbPlugin } from "@btst/db";
|
|
128
|
+
import { createEndpoint } from "better-call";
|
|
129
|
+
import { z } from "zod";
|
|
130
|
+
import {
|
|
131
|
+
defineBackendPlugin,
|
|
132
|
+
type BetterAuthDBSchema,
|
|
133
|
+
} from "@btst/stack/plugins";
|
|
134
|
+
|
|
135
|
+
// 1. Define your schema
|
|
136
|
+
const messagesSchema: BetterAuthDBSchema = {
|
|
137
|
+
messages: {
|
|
138
|
+
modelName: "Message",
|
|
139
|
+
fields: {
|
|
140
|
+
id: {
|
|
141
|
+
type: "number",
|
|
142
|
+
unique: true,
|
|
143
|
+
required: true,
|
|
144
|
+
},
|
|
145
|
+
content: {
|
|
146
|
+
type: "string",
|
|
147
|
+
required: true,
|
|
148
|
+
},
|
|
149
|
+
userId: {
|
|
150
|
+
type: "string",
|
|
151
|
+
required: true,
|
|
152
|
+
},
|
|
153
|
+
createdAt: {
|
|
154
|
+
type: "number",
|
|
155
|
+
required: true,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// 2. Create and export the backend plugin with full type inference
|
|
162
|
+
export const messagesBackendPlugin = defineBackendPlugin({
|
|
163
|
+
name: "messages",
|
|
164
|
+
dbPlugin: createDbPlugin("messages", messagesSchema),
|
|
165
|
+
routes: (adapter) => ({
|
|
166
|
+
// List messages
|
|
167
|
+
list: createEndpoint(
|
|
168
|
+
"/messages",
|
|
169
|
+
{
|
|
170
|
+
method: "GET",
|
|
171
|
+
query: z.object({
|
|
172
|
+
userId: z.string().optional(),
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
async ({ query }) => {
|
|
176
|
+
const messages = await adapter.findMany({
|
|
177
|
+
model: "messages",
|
|
178
|
+
where: query.userId
|
|
179
|
+
? [{ field: "userId", value: query.userId, operator: "eq" }]
|
|
180
|
+
: undefined,
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
status: 200,
|
|
184
|
+
body: messages,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
|
|
189
|
+
// Create message
|
|
190
|
+
create: createEndpoint(
|
|
191
|
+
"/messages",
|
|
192
|
+
{
|
|
193
|
+
method: "POST",
|
|
194
|
+
body: z.object({
|
|
195
|
+
content: z.string().min(1),
|
|
196
|
+
userId: z.string().min(1),
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
async ({ body }) => {
|
|
200
|
+
const message = await adapter.create({
|
|
201
|
+
model: "messages",
|
|
202
|
+
data: {
|
|
203
|
+
...body,
|
|
204
|
+
id: Date.now(),
|
|
205
|
+
createdAt: Date.now(),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
status: 201,
|
|
210
|
+
body: message,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
|
|
215
|
+
// Delete message
|
|
216
|
+
delete: createEndpoint(
|
|
217
|
+
"/messages/:id",
|
|
218
|
+
{
|
|
219
|
+
method: "DELETE",
|
|
220
|
+
params: z.object({
|
|
221
|
+
id: z.coerce.number(),
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
async ({ params }) => {
|
|
225
|
+
await adapter.delete({
|
|
226
|
+
model: "messages",
|
|
227
|
+
where: [{ field: "id", value: params.id, operator: "eq" }],
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
status: 204,
|
|
231
|
+
body: null,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Example: Messages Plugin (Client)
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// plugins/messages/client.ts
|
|
243
|
+
import { defineClientPlugin } from "@btst/stack/plugins";
|
|
244
|
+
import { createRoute } from "@btst/yar";
|
|
245
|
+
import { useQuery } from "@tanstack/react-query";
|
|
246
|
+
|
|
247
|
+
// Components
|
|
248
|
+
const MessagesPage = ({ data }: { data: any }) => (
|
|
249
|
+
<div>{/* Render messages */}</div>
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Create and export the client plugin with full type inference
|
|
253
|
+
export const messagesClientPlugin = defineClientPlugin({
|
|
254
|
+
name: "messages",
|
|
255
|
+
routes: () => ({
|
|
256
|
+
// Use Yar's createRoute for proper route creation
|
|
257
|
+
messagesList: createRoute(
|
|
258
|
+
"/messages",
|
|
259
|
+
() => ({
|
|
260
|
+
PageComponent: MessagesPage,
|
|
261
|
+
loader: async () => {
|
|
262
|
+
const response = await fetch("/api/messages");
|
|
263
|
+
return response.json();
|
|
264
|
+
},
|
|
265
|
+
meta: (data) => [
|
|
266
|
+
{ name: "title", content: "Messages" },
|
|
267
|
+
{ name: "description", content: `${data.length} messages` },
|
|
268
|
+
],
|
|
269
|
+
})
|
|
270
|
+
),
|
|
271
|
+
}),
|
|
272
|
+
hooks: () => ({
|
|
273
|
+
useMessages: () => {
|
|
274
|
+
// Use React Query or your preferred data fetching library
|
|
275
|
+
return useQuery({
|
|
276
|
+
queryKey: ["messages"],
|
|
277
|
+
queryFn: async () => {
|
|
278
|
+
const response = await fetch("/api/messages");
|
|
279
|
+
return response.json();
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
> **💡 Type Safety Tip:** Using `defineClientPlugin` and `defineBackendPlugin` helpers ensures full type inference without needing type annotations or casts. Your route keys, hook names, and endpoint types are automatically preserved!
|
|
288
|
+
|
|
289
|
+
### Using Your Plugins
|
|
290
|
+
|
|
291
|
+
Backend and client plugins are used independently in their respective contexts:
|
|
292
|
+
|
|
293
|
+
#### Backend Usage
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// app/api/route.ts (Backend only)
|
|
297
|
+
import { betterStack } from "@btst/stack/api";
|
|
298
|
+
import { messagesBackendPlugin } from "./plugins/messages/backend";
|
|
299
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
300
|
+
|
|
301
|
+
const api = betterStack({
|
|
302
|
+
plugins: {
|
|
303
|
+
messages: messagesBackendPlugin,
|
|
304
|
+
},
|
|
305
|
+
adapter: createMemoryAdapter,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
export const GET = api.handler;
|
|
309
|
+
export const POST = api.handler;
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Client Usage
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// app/lib/client.ts (Client only)
|
|
316
|
+
import { createStackClient } from "@btst/stack/client";
|
|
317
|
+
import { messagesClientPlugin } from "./plugins/messages/client";
|
|
318
|
+
|
|
319
|
+
const client = createStackClient({
|
|
320
|
+
plugins: {
|
|
321
|
+
messages: messagesClientPlugin,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
export const { router, hooks } = client;
|
|
326
|
+
|
|
327
|
+
// Use in components
|
|
328
|
+
// const { useMessages } = hooks.messages;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Core Concepts
|
|
332
|
+
|
|
333
|
+
### 1. Plugins
|
|
334
|
+
|
|
335
|
+
Plugins are self-contained features that can be composed together. Each plugin can provide:
|
|
336
|
+
|
|
337
|
+
- **Database Schema** - Table definitions using Better DB
|
|
338
|
+
- **API Endpoints** - Type-safe REST endpoints using Better Call
|
|
339
|
+
- **Client Routes** - Page routing using Yar Router
|
|
340
|
+
- **React Hooks** - Data fetching and mutations
|
|
341
|
+
|
|
342
|
+
### 2. Adapters
|
|
343
|
+
|
|
344
|
+
Adapters connect your plugins to different databases:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Memory (for testing)
|
|
348
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
349
|
+
|
|
350
|
+
// PostgreSQL
|
|
351
|
+
import { createPgAdapter } from "@btst/adapter-pg";
|
|
352
|
+
|
|
353
|
+
// MongoDB
|
|
354
|
+
import { createMongoAdapter } from "@btst/adapter-mongo";
|
|
355
|
+
|
|
356
|
+
const api = betterStack({
|
|
357
|
+
plugins: { /* ... */ },
|
|
358
|
+
adapter: createMemoryAdapter, // or createPgAdapter, createMongoAdapter
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 3. Type Safety
|
|
363
|
+
|
|
364
|
+
Better Stack provides end-to-end type safety with automatic type inference:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// ✅ Backend: Route keys and endpoint types are preserved
|
|
368
|
+
const myPlugin = defineBackendPlugin({
|
|
369
|
+
name: "users",
|
|
370
|
+
routes: (adapter) => ({
|
|
371
|
+
getUser: createEndpoint(
|
|
372
|
+
"/users/:id",
|
|
373
|
+
{
|
|
374
|
+
method: "GET",
|
|
375
|
+
params: z.object({ id: z.string() }),
|
|
376
|
+
},
|
|
377
|
+
async ({ params }) => {
|
|
378
|
+
// params.id is typed as string
|
|
379
|
+
return { status: 200, body: { id: params.id, name: "John" } };
|
|
380
|
+
}
|
|
381
|
+
),
|
|
382
|
+
listUsers: createEndpoint(/* ... */),
|
|
383
|
+
})
|
|
384
|
+
});
|
|
385
|
+
// Type: BackendPlugin<{ getUser: Endpoint, listUsers: Endpoint }>
|
|
386
|
+
|
|
387
|
+
// ✅ Client: Route keys and hook types are preserved
|
|
388
|
+
const myClientPlugin = defineClientPlugin({
|
|
389
|
+
name: "users",
|
|
390
|
+
routes: () => ({
|
|
391
|
+
userProfile: { path: "/users/:id", Component: UserProfile },
|
|
392
|
+
usersList: { path: "/users", Component: UsersList },
|
|
393
|
+
}),
|
|
394
|
+
hooks: () => ({
|
|
395
|
+
useUser: (id: string) => { /* ... */ },
|
|
396
|
+
useUsers: () => { /* ... */ },
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
// Routes "userProfile" and "usersList" are fully typed!
|
|
400
|
+
// Hooks "useUser" and "useUsers" are accessible via hooks.users.*
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Plugin Utilities
|
|
405
|
+
|
|
406
|
+
Better Stack exports utilities for building plugins:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// Type-safe plugin helpers (recommended)
|
|
410
|
+
import {
|
|
411
|
+
defineBackendPlugin,
|
|
412
|
+
defineClientPlugin,
|
|
413
|
+
} from "@btst/stack/plugins";
|
|
414
|
+
|
|
415
|
+
// Type definitions for backend plugins
|
|
416
|
+
import type {
|
|
417
|
+
BackendPlugin,
|
|
418
|
+
Adapter,
|
|
419
|
+
DatabaseDefinition,
|
|
420
|
+
Endpoint,
|
|
421
|
+
Router,
|
|
422
|
+
} from "@btst/stack/plugins";
|
|
423
|
+
|
|
424
|
+
// Type definitions for client plugins
|
|
425
|
+
import type {
|
|
426
|
+
ClientPlugin,
|
|
427
|
+
Route,
|
|
428
|
+
} from "@btst/stack/plugins";
|
|
429
|
+
|
|
430
|
+
// Utilities (can be used in both)
|
|
431
|
+
import {
|
|
432
|
+
createApiClient,
|
|
433
|
+
} from "@btst/stack/plugins";
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Type-Safe Plugin Helpers
|
|
437
|
+
|
|
438
|
+
Use `defineBackendPlugin` and `defineClientPlugin` for automatic type inference:
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// ✅ Recommended: Full type inference, no casts needed
|
|
442
|
+
const myPlugin = defineBackendPlugin({
|
|
443
|
+
name: "myFeature",
|
|
444
|
+
routes: (adapter) => ({
|
|
445
|
+
list: endpoint(...),
|
|
446
|
+
create: endpoint(...),
|
|
447
|
+
})
|
|
448
|
+
});
|
|
449
|
+
// Route keys "list" and "create" are fully typed!
|
|
450
|
+
|
|
451
|
+
// ❌ Old way: Manual type annotation
|
|
452
|
+
const myPlugin: BackendPlugin = {
|
|
453
|
+
name: "myFeature",
|
|
454
|
+
routes: (adapter) => ({
|
|
455
|
+
list: endpoint(...),
|
|
456
|
+
create: endpoint(...),
|
|
457
|
+
})
|
|
458
|
+
};
|
|
459
|
+
// Route keys are erased to generic Record<string, Endpoint>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Why Separate Backend and Client Plugins?
|
|
463
|
+
|
|
464
|
+
**Traditional Approach (Unified Plugin):**
|
|
465
|
+
```typescript
|
|
466
|
+
// ❌ This can cause SSR issues and bundle bloat
|
|
467
|
+
export const myPlugin = {
|
|
468
|
+
backend: { /* server-side code */ },
|
|
469
|
+
client: { /* client-side code */ },
|
|
470
|
+
};
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Better Stack Approach (Separated Plugins):**
|
|
474
|
+
```typescript
|
|
475
|
+
// ✅ Backend stays on the server
|
|
476
|
+
// plugins/my-plugin/backend.ts
|
|
477
|
+
export const myBackendPlugin: BackendPlugin = { /* ... */ };
|
|
478
|
+
|
|
479
|
+
// ✅ Client stays on the client
|
|
480
|
+
// plugins/my-plugin/client.ts
|
|
481
|
+
export const myClientPlugin: ClientPlugin = { /* ... */ };
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Benefits:**
|
|
485
|
+
1. **No SSR Issues** - Server code never reaches the client bundle
|
|
486
|
+
2. **Better Code Splitting** - Frontend and backend can be deployed separately
|
|
487
|
+
3. **Smaller Bundles** - Client bundles don't include server dependencies
|
|
488
|
+
4. **Independent Versioning** - Update backend without touching frontend
|
|
489
|
+
5. **Type Safety** - TypeScript ensures correct usage in each context
|
|
490
|
+
|
|
491
|
+
### API Client Utility
|
|
492
|
+
|
|
493
|
+
Create a typed API client for server-side or client-side requests:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { createApiClient } from "@btst/stack/plugins";
|
|
497
|
+
|
|
498
|
+
// For server-side (SSR)
|
|
499
|
+
const api = createApiClient({
|
|
500
|
+
baseURL: "http://localhost:3000",
|
|
501
|
+
basePath: "/api",
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// For client-side (uses relative URLs)
|
|
505
|
+
const api = createApiClient({
|
|
506
|
+
basePath: "/api",
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Testing Your Plugins
|
|
511
|
+
|
|
512
|
+
Better Stack includes comprehensive testing utilities:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// __tests__/my-plugin.test.ts
|
|
516
|
+
import { describe, it, expect } from "vitest";
|
|
517
|
+
import { betterStack } from "@btst/stack/api";
|
|
518
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
519
|
+
import { myBackendPlugin } from "../plugins/my-plugin/backend";
|
|
520
|
+
|
|
521
|
+
describe("My Backend Plugin", () => {
|
|
522
|
+
it("should create and retrieve items", async () => {
|
|
523
|
+
// Adapter wrapper for testing
|
|
524
|
+
const testAdapter = (db) => createMemoryAdapter(db)({});
|
|
525
|
+
|
|
526
|
+
const api = betterStack({
|
|
527
|
+
plugins: { myFeature: myBackendPlugin },
|
|
528
|
+
adapter: testAdapter,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Create an item
|
|
532
|
+
const createResponse = await api.handler(
|
|
533
|
+
new Request("http://localhost:3000/api/items", {
|
|
534
|
+
method: "POST",
|
|
535
|
+
headers: { "Content-Type": "application/json" },
|
|
536
|
+
body: JSON.stringify({ name: "Test Item" }),
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(createResponse.status).toBe(201);
|
|
541
|
+
|
|
542
|
+
// Retrieve items
|
|
543
|
+
const listResponse = await api.handler(
|
|
544
|
+
new Request("http://localhost:3000/api/items", {
|
|
545
|
+
method: "GET",
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const items = await listResponse.json();
|
|
550
|
+
expect(items).toHaveLength(1);
|
|
551
|
+
expect(items[0].name).toBe("Test Item");
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Framework Integration
|
|
557
|
+
|
|
558
|
+
### Next.js App Router
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// app/api/[...route]/route.ts
|
|
562
|
+
import { betterStack } from "@btst/stack/api";
|
|
563
|
+
|
|
564
|
+
const api = betterStack({
|
|
565
|
+
plugins: { /* ... */ },
|
|
566
|
+
adapter: /* ... */,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
export const GET = api.handler;
|
|
570
|
+
export const POST = api.handler;
|
|
571
|
+
export const PUT = api.handler;
|
|
572
|
+
export const DELETE = api.handler;
|
|
573
|
+
export const PATCH = api.handler;
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Next.js Pages Router
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
// pages/api/[...route].ts
|
|
580
|
+
import { betterStack } from "@btst/stack/api";
|
|
581
|
+
|
|
582
|
+
const api = betterStack({
|
|
583
|
+
plugins: { /* ... */ },
|
|
584
|
+
adapter: /* ... */,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
588
|
+
const request = new Request(
|
|
589
|
+
`http://localhost:3000${req.url}`,
|
|
590
|
+
{
|
|
591
|
+
method: req.method,
|
|
592
|
+
headers: req.headers as HeadersInit,
|
|
593
|
+
body: req.method !== "GET" ? JSON.stringify(req.body) : undefined,
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const response = await api.handler(request);
|
|
598
|
+
const data = await response.json();
|
|
599
|
+
|
|
600
|
+
res.status(response.status).json(data);
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Standalone Express/Node.js
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import express from "express";
|
|
608
|
+
import { betterStack } from "@btst/stack/api";
|
|
609
|
+
|
|
610
|
+
const api = betterStack({
|
|
611
|
+
plugins: { /* ... */ },
|
|
612
|
+
adapter: /* ... */,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const app = express();
|
|
616
|
+
|
|
617
|
+
app.all("/api/*", async (req, res) => {
|
|
618
|
+
const request = new Request(`http://localhost:3000${req.url}`, {
|
|
619
|
+
method: req.method,
|
|
620
|
+
headers: req.headers as HeadersInit,
|
|
621
|
+
body: req.method !== "GET" ? JSON.stringify(req.body) : undefined,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const response = await api.handler(request);
|
|
625
|
+
const data = await response.json();
|
|
626
|
+
|
|
627
|
+
res.status(response.status).json(data);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
app.listen(3000);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## Architecture
|
|
634
|
+
|
|
635
|
+
Better Stack is built on top of proven libraries:
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
┌─────────────────────────────────────────┐
|
|
639
|
+
│ Better Stack │
|
|
640
|
+
│ (Plugin Composition & Type Safety) │
|
|
641
|
+
├─────────────────────────────────────────┤
|
|
642
|
+
│ Better Call │ Better DB │ Yar │
|
|
643
|
+
│ (API Layer) │ (Database) │ (Router) │
|
|
644
|
+
└─────────────────────────────────────────┘
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Core Libraries
|
|
648
|
+
|
|
649
|
+
- **[Better Call](https://github.com/olliethedev/better-call)** - Type-safe API endpoints with automatic request/response handling
|
|
650
|
+
- **[Better DB](https://github.com/olliethedev/better-auth)** - Schema-first database ORM with multiple adapter support
|
|
651
|
+
- **[Yar Router](https://github.com/olliethedev/yar)** - Lightweight client-side router for React
|
|
652
|
+
|
|
653
|
+
## Publishing Your Plugin
|
|
654
|
+
|
|
655
|
+
Want to share your plugin with the community? Publish backend and client as separate packages for maximum flexibility.
|
|
656
|
+
|
|
657
|
+
### Publishing Backend Plugin
|
|
658
|
+
|
|
659
|
+
1. Create backend package:
|
|
660
|
+
```bash
|
|
661
|
+
mkdir my-plugin-backend
|
|
662
|
+
cd my-plugin-backend
|
|
663
|
+
npm init
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
2. Structure:
|
|
667
|
+
```
|
|
668
|
+
my-plugin-backend/
|
|
669
|
+
├── src/
|
|
670
|
+
│ └── index.ts # Export BackendPlugin
|
|
671
|
+
├── package.json
|
|
672
|
+
└── tsconfig.json
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
3. Add peer dependencies:
|
|
676
|
+
```json
|
|
677
|
+
{
|
|
678
|
+
"name": "@yourorg/my-plugin-backend",
|
|
679
|
+
"peerDependencies": {
|
|
680
|
+
"@btst/stack": "^1.0.0",
|
|
681
|
+
"@btst/db": "^1.0.0",
|
|
682
|
+
"better-call": "^1.0.19"
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Publishing Client Plugin
|
|
688
|
+
|
|
689
|
+
1. Create client package:
|
|
690
|
+
```bash
|
|
691
|
+
mkdir my-plugin-client
|
|
692
|
+
cd my-plugin-client
|
|
693
|
+
npm init
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
2. Structure:
|
|
697
|
+
```
|
|
698
|
+
my-plugin-client/
|
|
699
|
+
├── src/
|
|
700
|
+
│ └── index.tsx # Export ClientPlugin
|
|
701
|
+
├── package.json
|
|
702
|
+
└── tsconfig.json
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
3. Add peer dependencies:
|
|
706
|
+
```json
|
|
707
|
+
{
|
|
708
|
+
"name": "@yourorg/my-plugin-client",
|
|
709
|
+
"peerDependencies": {
|
|
710
|
+
"@btst/stack": "^1.0.0",
|
|
711
|
+
"@btst/yar": "^1.1.1",
|
|
712
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
4. Users can install only what they need:
|
|
718
|
+
```bash
|
|
719
|
+
# Server-only deployment
|
|
720
|
+
npm install @yourorg/my-plugin-backend
|
|
721
|
+
|
|
722
|
+
# Client-only deployment
|
|
723
|
+
npm install @yourorg/my-plugin-client
|
|
724
|
+
|
|
725
|
+
# Full-stack deployment
|
|
726
|
+
npm install @yourorg/my-plugin-backend @yourorg/my-plugin-client
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
## Examples
|
|
730
|
+
|
|
731
|
+
Check out example plugins and applications:
|
|
732
|
+
|
|
733
|
+
- **Messages Plugin** - See `src/__tests__/plugins.test.ts` for a complete example
|
|
734
|
+
- **Coming Soon**: Example applications in the `examples/` directory
|
|
735
|
+
|
|
736
|
+
## API Reference
|
|
737
|
+
|
|
738
|
+
### Backend API
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
betterStack(config: BackendLibConfig): BackendLib
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Creates a backend instance with plugins and returns an API handler.
|
|
745
|
+
|
|
746
|
+
**Parameters:**
|
|
747
|
+
- `plugins` - Record of plugin instances
|
|
748
|
+
- `adapter` - Database adapter function
|
|
749
|
+
- `dbSchema` - (Optional) Additional database schema
|
|
750
|
+
|
|
751
|
+
**Returns:**
|
|
752
|
+
- `handler` - Request handler for your framework
|
|
753
|
+
- `router` - Better Call router instance
|
|
754
|
+
- `dbSchema` - Combined database schema
|
|
755
|
+
|
|
756
|
+
### Client API
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
createStackClient<TPlugins>(config: ClientLibConfig<TPlugins>): ClientLib
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Creates a client instance with plugins and returns router and hooks.
|
|
763
|
+
|
|
764
|
+
**Parameters:**
|
|
765
|
+
- `plugins` - Record of plugin instances
|
|
766
|
+
- `baseURL` - (Optional) Base URL for API calls
|
|
767
|
+
- `basePath` - (Optional) API path prefix (default: "/api")
|
|
768
|
+
|
|
769
|
+
**Returns:**
|
|
770
|
+
- `router` - Yar router instance
|
|
771
|
+
- `hooks` - Plugin hooks organized by plugin name
|
|
772
|
+
|
|
773
|
+
## Contributing
|
|
774
|
+
|
|
775
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
776
|
+
|
|
777
|
+
## License
|
|
778
|
+
|
|
779
|
+
MIT © [olliethedev](https://github.com/olliethedev)
|
|
780
|
+
|
|
781
|
+
## Support
|
|
782
|
+
|
|
783
|
+
- 📖 [Documentation](https://github.com/olliethedev/better-stack)
|
|
784
|
+
- 🐛 [Issue Tracker](https://github.com/olliethedev/better-stack/issues)
|
|
785
|
+
- 💬 [Discussions](https://github.com/olliethedev/better-stack/discussions)
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
<div align="center">
|
|
790
|
+
<strong>Built with ❤️ using Better Stack</strong>
|
|
791
|
+
</div>
|