uploads
Accept file uploads with size limits, store them on disk or in the cloud, and show progress in the browser.
Avatars, attachments, imports: sooner or later an app takes files from people. kitr/uploads does the whole round trip. The browser checks size and type before sending anything and shows progress. The server checks again (never trust the browser), stores the bytes through a driver and answers with a file record you can save in your database.
Install
kitr add uploadsFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
The recipe writes config/uploads.ts and adds mountUploads(server) to server/main.ts: POST /api/uploads receives files and GET /api/uploads/:key serves them.
// config/uploads.ts
import { defineConfig } from 'kitr/uploads'
import { createDiskUploadsDriver } from 'kitr/uploads/server'
export default defineConfig({
driver: createDiskUploadsDriver({ dir: 'storage/uploads' }),
maxSize: 10 * 1024 * 1024,
accept: ['image/*', '.pdf'],
maxFiles: 1,
})
In a component
import { useUpload } from 'kitr/uploads/react'
function Avatar() {
const { send, progress, uploading, error, files } = useUpload({ accept: ['image/*'] })
return (
<>
<input type="file" accept="image/*" onChange={(e) => send(e.target.files)} />
{uploading ? <progress value={progress} max={100} /> : null}
{files[0] ? <img src={files[0].url} alt="" /> : null}
{error ? <p>{error.message}</p> : null}
</>
)
}
upload(file, { url, onProgress }) is the same without React.
On the server
// a controller that accepts a document for a project
async store({ event, params, app }: HttpContext) {
const [file] = await useUploads(app).receive(event, { accept: ['.pdf'], maxSize: 20e6 })
await db.documents.create({ projectId: params.id, key: file.key, name: file.name })
return file
}
receive() throws KitrError.validation with the field name when a rule fails, so forms show it under the input. store() writes bytes you already have; read() and remove() complete the set.
Drivers
The disk driver writes under a directory and serves files through the mounted GET route. Object storage is a driver with put, get, delete and an optional url(key) for signed or CDN URLs.
| Option | Default | Description |
|---|---|---|
driver |
disk, under storage/uploads |
Where files go. |
maxSize |
none | Bytes per file. |
accept |
anything | MIME types (image/*, application/pdf) or extensions (.pdf). |
maxFiles |
none | Files per request. |
publicPath |
none | The URL prefix files are served under, such as /uploads. |
The same rules apply in both places: useUpload() checks them before sending, and the server checks them again on receipt. Pass stricter rules to receive() for one route.
Errors
In the browser, a rule that fails or a server rejection comes back as an UploadError in error, with a readable message like "photo.heic is not an accepted type". Show it as it is. checkFile(file, rules) runs the same checks yourself, and formatBytes(10485760) gives "10 MB" for a hint under the input.
Tests
createMemoryUploadsDriver() keeps files in memory. Use it in config/uploads.ts for tests, and post files with the testing client.