feat: beta API redesign (v0.6.0)#179
Draft
abhay-ramesh wants to merge 9 commits intomainfrom
Draft
Conversation
…#171) ## Problem Server and client used different names for the same lifecycle events: | Event | Server (before) | Client (before) | |---|---|---| | Upload starts | `onUploadStart` | `onStart` | | Progress update | `onUploadProgress` | `onProgress` | | Upload completes | `onUploadComplete` | `onSuccess` | | Upload fails | `onUploadError` | `onError` | This forced developers to memorize two vocabularies and made copy-pasting between server/client configs error-prone. ## Changes New unified API — same names everywhere: - `.onStart()` — replaces `.onUploadStart()` - `.onProgress()` — replaces `.onUploadProgress()` - `.onComplete()` — replaces `.onUploadComplete()` AND client `onSuccess` - `.onError()` — replaces `.onUploadError()` Old names kept with `console.warn()` deprecation notices. All existing code continues to work. ## Files changed - `src/core/router/router-v2.ts` — S3Route class + S3RouteConfig + invocation sites - `src/core/schema.ts` — S3FileSchema lifecycle method delegates - `src/types/index.ts` — UploadRouteConfig interface (added `onComplete`) - `src/hooks/use-upload-route.ts` — reads `onComplete` with fallback to `onSuccess` - `src/__tests__/expires-in.test.ts` — updated to use new field names ## Test plan - [x] All 156 existing tests pass - [x] Type check clean - [x] Old methods (`onUploadComplete` etc.) still work, just log a deprecation warning 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…l) (#172) ## Problem `S3UploadedFile` had `url` and `key` with no indication of which to store in a database: - `url` — could be a presigned URL (expires in ~1h) or a public URL depending on config - `key` — the permanent S3 key, but nothing told users this was the safe one to store This caused a common bug: users store `url` in their DB, links break after an hour. ## Changes Three explicit fields with clear JSDoc: | Field | Meaning | Store in DB? | |---|---|---| | `storagePath` | S3 key/path (e.g. `uploads/user/photo.jpg`) | ✅ Yes — never expires | | `publicUrl` | Public URL when bucket has public access | ✅ Yes — never expires | | `presignedUrl` | Temporary download URL | ❌ No — expires in ~1h | Old `key` and `url` fields kept with `@deprecated` JSDoc — same values, no behavior change. Applied to: `S3UploadedFile`, `S3LifecycleContext`, `CompletionResponse`. ## Test plan - [x] All 156 tests pass - [x] Type check clean - [x] Old `key`/`url` fields still present and populated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Problem
`uploadFiles()` returned `void` — to get upload results you had to
either:
- Pass an `onSuccess` callback (callback hell)
- Watch the `files` state array (works for UI but awkward for logic)
```ts
// Before — forced callback pattern
const { uploadFiles } = useUploadRoute('avatars', {
onSuccess: (results) => {
db.save({ path: results[0].key }); // can't await this naturally
}
});
await uploadFiles(files); // returns nothing
```
## Change
`uploadFiles()` now returns `Promise<S3UploadedFile[]>`:
```ts
// After — direct await
const { uploadFiles } = useUploadRoute('avatars');
const results = await uploadFiles(files);
await db.save({ path: results[0].key });
```
## Backward compatibility
- Existing `await uploadFiles(files)` with void usage still works (just
ignores the return)
- `onSuccess` callback still fires as before
- Returns `[]` on error (error is still reported via `errors` state and
`onError` callback)
## Test plan
- [x] All 156 tests pass
- [x] Type check clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Problem
Three inconsistencies in the chainable schema API:
1. `.types(['image/jpeg'])` and `.extensions(['.pdf'])` are separate
methods with no unified shorthand — but the HTML `<input accept>`
attribute handles both in one attribute
2. `.maxFileSize()` is verbose; `.min()` is short — asymmetric naming
3. Object form uses `allowedTypes` but chain uses `.types()` — different
keys for same concept
## Changes
### `.accept(types)` — replaces `.types()` and `.extensions()`
Mirrors `<input accept>`. Works with MIME types, wildcards, and
extensions:
```ts
upload.file().accept('image/*')
upload.file().accept(['image/jpeg', 'application/pdf'])
upload.file().accept(['.pdf', '.doc'])
upload.file().accept(['image/*', '.pdf']) // mixed
// Also works in object form
upload.file({ accept: ['image/*', '.pdf'] })
```
### `.maxSize(size)` — replaces `.maxFileSize()`
```ts
upload.file().maxSize('10MB') // was: .maxFileSize('10MB')
upload.image().maxSize('5MB')
```
### `.minSize(size)` — replaces `.min()`
```ts
upload.file().minSize('1KB') // was: .min('1KB')
```
## Backward compatibility
All old methods kept with `console.warn()` deprecation notices:
- `.types()` → `.accept()`
- `.extensions()` → `.accept(['.ext'])`
- `.maxFileSize()` → `.maxSize()`
- `.max()` → `.maxSize()` (was already deprecated, warning message
updated)
- `.min()` → `.minSize()`
## Test plan
- [x] All 156 tests pass
- [x] Type check clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Problem
`createUploadConfig().build()` returned `{ s3 }`. When using Cloudflare
R2, MinIO, or DigitalOcean Spaces:
```ts
const { s3 } = createUploadConfig()
.provider('cloudflare-r2', { ... })
.build();
s3.file() // Why is R2 called s3?
```
This contradicts the library's "provider-agnostic" positioning and
confuses developers not using AWS.
## Change
`build()` now returns `{ upload, s3 }`:
```ts
const { upload } = createUploadConfig()
.provider('cloudflare-r2', { ... })
.build();
upload.file().maxSize('10MB') // Clear — works with any provider
```
`s3` is kept as a deprecated getter on the return object:
- Existing `const { s3 } = ...build()` code continues to work
- Logs a `console.warn()` pointing to `upload` as the new name
## No internal changes
The `upload` object is the exact same thing as `s3` was — purely a
renaming of the destructured property.
## Test plan
- [x] All 156 tests pass
- [x] Type check clean
- [x] `const { s3 }` still works with deprecation warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…176) ## Problem `.paths()` and `.expiresIn()` mutated `this.config` in place, while `.middleware()` correctly created a new `S3Route`. This inconsistency could silently corrupt a shared base route: ```ts const base = s3.file().maxSize('10MB').middleware(auth); const docsRoute = base.paths({ prefix: 'docs' }); // mutated base! const imagesRoute = base.paths({ prefix: 'images' }); // base was already corrupted ``` ## Fix Both methods now clone the config into a new `S3Route` instance — matching the behavior of `.middleware()`, `.refine()`, and `.transform()`: ```ts const base = upload.file().maxSize('10MB').middleware(auth); const docsRoute = base.paths({ prefix: 'docs' }); // new instance ✓ const imagesRoute = base.paths({ prefix: 'images' }); // base untouched ✓ ``` ## Impact This is technically a behavior change, but only visible if code was relying on mutation (which was always a bug). The immutable pattern matches every other method on the chain and the mental model of Zod-style builders. ## Test plan - [x] All 156 tests pass (including the existing `expiresIn` config preservation tests) - [x] Type check clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
- Add `useUpload<TRouter>(routeName, config?)` as the preferred hook
name
- The existing `upload.routeName()` proxy pattern is invisible to
linters and looks like a regular method call rather than a React hook
- Export `useUpload` from both `pushduck` and `pushduck/client`
- `useUploadRoute` kept unchanged for full backward compatibility
## Before / After
**Before** (proxy pattern — linter can't see it's a hook):
```ts
const upload = createUploadClient<AppRouter>({ endpoint: '/api/upload' });
const { uploadFiles, files } = upload.imageUpload(); // looks like a method call
```
**After** (explicit hook):
```ts
import { useUpload } from 'pushduck/client';
const { uploadFiles, files } = useUpload<AppRouter>('imageUpload'); // clearly a hook
```
## Test plan
- [x] All 156 existing tests pass
- [x] Type-check passes with no errors
- [x] Both overload signatures (generic and string) work correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
- Add short, provider-neutral type aliases alongside the existing
`S3`-prefixed types
- Users on Cloudflare R2, MinIO, or other providers shouldn't need `S3*`
names everywhere
- All old names remain 100% supported — no deprecation, purely additive
## New aliases
| Old (still works) | New alias |
|---|---|
| `S3Router` | `UploadRouter` |
| `S3UploadedFile` | `UploadedFile` |
| `S3RouteUploadResult` | `UploadResult` |
| `RouterRouteNames` | `RouteNames` |
| `S3FileConstraints` | `FileConstraints` |
| `S3LifecycleContext` | `LifecycleContext` |
| `S3LifecycleHook` | `LifecycleHook` |
| `S3Middleware` | `Middleware` |
| `S3MiddlewareContext` | `MiddlewareContext` |
| `S3RouteContext` | `RouteContext` |
| `S3RouterDefinition` | `RouterDefinition` |
## Before / After
**Before** (reads as AWS-specific):
```ts
import type { S3Router, S3LifecycleContext } from 'pushduck/server';
```
**After** (provider-neutral):
```ts
import type { UploadRouter, LifecycleContext } from 'pushduck/server';
```
## Test plan
- [x] All 156 tests pass
- [x] Type-check passes with no errors
- [x] Both old and new names resolve to identical types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This draft PR tracks all API improvements landing in the
betabranch before being merged tomainasv0.6.0.Do not merge directly — this is a tracking PR. When
betais stable and ready to graduate, this PR gets reviewed and merged.Changes included
onComplete,onStart,onProgress,onErroron server + clientstoragePath(store in DB),publicUrl,presignedUrl(don't store)uploadFiles()returnsPromise<S3UploadedFile[]>instead ofvoid.accept(),.maxSize(),.minSize()upload.*as primary,s3.*deprecated.paths()and.expiresIn()return new instances instead of mutatinguseUploadexplicit hook API — visible to linters, clearly a hookUploadRouter,LifecycleContext,Middleware, etc.API transformation summary
Hooks
Schema
Results
Return values
Types
Backward compatibility
Every old API still works. Old names emit
console.warn()deprecation notices. Nothing is removed until1.0.0.Publishing beta to npm (when ready)
Run the Create Release workflow on the
betabranch with typeprereleaseand tagbeta:🤖 Generated with Claude Code