# The library

> The colis Node package, the piece under the delivery page: a file API over your bucket, the transfer routes as one route file, small records with an expiry, conditional writes and stable error codes.

Canonical: https://colis-site.vercel.app/en/library · Markdown: https://colis-site.vercel.app/en/library.md · Français: https://colis-site.vercel.app/library

The package the delivery page is built on: a file API over your bucket, the transfer routes as one route file, and small records beside each file. The only package that holds credentials, so the only one that runs on your server.

Not on npm yet: the packages are built from [the repository](https://github.com/mamadouwhile/colis) until they are published.

```ts
import { createBucket, createTransferHandler } from '@colis/core'

const store = createBucket({ bucket: 'livraisons' })

// The transfer routes on your own domain, in one route file.
export const { GET, POST, DELETE } = createTransferHandler({
  bucket: store,
  expiresIn: 7 * 24 * 3600,
  authorize: (request) => request.headers.get('authorization') === `Bearer ${process.env.TOKEN}`,
})
```

## Five verbs over your bucket.
Strings, buffers, Blobs and streams are all accepted. Keys round-trip: what upload() returns is what you hand back to get(), getUrl() and delete().

```ts
await store.upload(file)     // → { key, path, url?, size?, etag?, contentType }
await store.put(id, file)    // one file per identifier: create or replace
await store.get(id)          // → the file back, or null
await store.getUrl(id)       // → public or presigned URL
await store.delete(id)       // → void

await store.put(code, file, { ifAbsent: true }) // claim a code, never overwrite one
store.client                 // the plain S3Client, for anything else
```

`getUrl()` returns a presigned URL by default, or an unsigned one when a `publicUrl` is configured, with a `download` option that sets the filename the browser saves.

`ifAbsent` writes only if nothing is stored under the key yet: that is how a fresh code is claimed without trampling one in use. A stream needs a `contentLength`, and `maxSize` refuses an oversized body before anything reaches the network.

Anything the package does not wrap is one command away through `store.client`, the plain `S3Client`.

## The transfer routes, in one route file.
createTransferHandler() serves the four-route protocol: create, read, download, burn. Request in, Response out: a Next route, a Hono route, Bun.serve or a worker, without an adapter.

**Next.js App Router**
```ts
// app/api/transfers/[[...route]]/route.ts
import { createBucket, createTransferHandler } from '@colis/core'

export const { GET, POST, DELETE } = createTransferHandler({
  bucket: createBucket({ bucket: 'livraisons' }),
  expiresIn: 7 * 24 * 3600,
  raw: 'redirect', // downloads 302 to a presigned URL
  authorize: (request) => request.headers.get('authorization') === `Bearer ${process.env.TOKEN}`,
})
```
**Hono**
```ts
import { Hono } from 'hono'
import { createBucket, createTransferHandler } from '@colis/core'

const transfers = createTransferHandler({ bucket: createBucket(), basePath: '/api/transfers' })

const app = new Hono()
app.all('/api/transfers', (c) => transfers(c.req.raw))
app.all('/api/transfers/*', (c) => transfers(c.req.raw))
```
**Bun.serve**
```ts
import { createBucket, createTransferHandler } from '@colis/core'

const transfers = createTransferHandler({ bucket: createBucket(), basePath: '/api/transfers' })

Bun.serve({
  fetch(request) {
    if (new URL(request.url).pathname.startsWith('/api/transfers')) return transfers(request)
    return new Response('Not found', { status: 404 })
  },
})
```

Every route is public unless you pass `authorize`. Return `false` for a plain 401, or a `Response` of your own. With `raw: 'redirect'` a download answers 302 with a presigned URL, so the bytes do not transit your server twice. The delivery page puts its password check, its receipt and its webhooks in front of this handler.

## Small JSON beside the file, with an expiry.
putSnapshot() stores a value in a self-describing envelope, gzipped, with your app name, a schema version and an expiry. The delivery page keeps its receipts this way.

```ts
// How the delivery page keeps a client's answer beside the transfer.
await store.putSnapshot(`meta/${code}.verdict`, { decision: 'approved', at }, {
  app: 'drop',
  expiresIn: 7 * 24 * 3600,
  ifAbsent: true, // one answer, never overwritten
})

const verdict = await store.getSnapshot(`meta/${code}.verdict`)
verdict?.data // → the record, or null when unknown or expired
```

- **null when expired** — An expired record is never handed over, even if the object is still in the bucket.
- **The first write wins** — With `ifAbsent`, a second write under the same key fails with `PRECONDITION_FAILED` instead of replacing the first: that is what makes a client’s answer final.

## Everything throws a ColisError with a stable code.
Failures that can be caught locally, a bad code, an oversized body, unserializable data, are raised before anything reaches the network.

```ts
import { isColisError } from '@colis/core'

try {
  await store.upload(body, { filename })
} catch (error) {
  if (isColisError(error) && error.code === 'FILE_TOO_LARGE') {
    return Response.json({ error: 'Too large to send in one piece' }, { status: 413 })
  }
  throw error
}
```

- `INVALID_SYNC_CODE`: Empty, or characters outside the alphabet
- `FILE_TOO_LARGE`: Body above the configured maxSize
- `PRECONDITION_FAILED`: An ifMatch or ifAbsent write lost the race
- `SNAPSHOT_TOO_NEW`: Schema version above the maxVersion given
- `INVALID_KEY / INVALID_BODY`: A key or a body type the bucket cannot take
- `UPLOAD_FAILED / GET_FAILED / …`: S3 rejected the request; the original error is in cause

## Every option, and the environment variable behind it.
createBucket() with no arguments works once COLIS_BUCKET and the usual AWS variables are set. With an endpoint, the region defaults to auto and path-style addressing turns on, which is what R2, MinIO and Scaleway expect.

```ts
createBucket({
  bucket: 'livraisons',         // or COLIS_BUCKET / S3_BUCKET
  region: 'eu-west-3',          // or COLIS_REGION / AWS_REGION
  credentials: { … },           // omit for the AWS provider chain
  endpoint: 'https://…',        // R2, MinIO, Scaleway, Wasabi — or COLIS_ENDPOINT
  prefix: 'drop',               // internal namespace
  maxSize: 4 * 1024 * 1024,     // reject before any network call
  syncCode: { length: 8 },      // the shape of store.codes
})
```

Through your server, an upload is bound by your runtime’s request limit: 4.5 MB on Vercel functions. Set `maxSize` just under it and an oversized upload costs a comparison instead of a truncated request; the delivery page sends anything larger straight to the bucket.

`createBucket()` is cheap: the underlying client is built on the first request, so calling it at module scope is fine.

