uploads

Accept file uploads with size limits, store them on disk or in the cloud, and show progress in the browser.

BackendClient Server

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 uploads

First 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.