Skip to content

Storage

Object storage shares the rest of the framework’s shape. Storage is a regular #[injectable] provider: inject it, call a handful of async methods. It hands clients short-lived presigned URLs so bytes never transit your API, and reads or writes objects server-side for workers that transform them.

The client ships in nest-rs-storage, built on object_store — the generic object-store abstraction maintained under Apache Arrow. It is multi-driver (S3, GCS, Azure, local filesystem, in-memory) behind one trait, with presigning in a separate Signer trait the S3 driver implements. The AWS-S3 driver is wired by default and speaks to real AWS S3 as well as any S3-compatible server (MinIO, RustFS) in path- or virtual-host style.

Terminal window
cargo add nest-rs-storage

StorageModule owns its StorageConfig (namespace storage, loaded from NESTRS_STORAGE__*) and registers Storage as a singleton provider. Import it where a feature needs storage:

src/media/module.rs
use nest_rs_core::module;
use nest_rs_storage::StorageModule;
use crate::media::MediaService;
#[module(imports = [StorageModule], providers = [MediaService])]
pub struct MediaModule;

The media slice below (a post cover-image upload) is the canonical storage shape.

Every field is settable via NESTRS_STORAGE__* env or a pinned StorageConfig — the framework-wide dual-path config rule.

KeyDefaultMeaning
ENDPOINThttp://rustfs:9000S3 endpoint; empty ⇒ real AWS S3
REGIONus-east-1region
ACCESS_KEY / SECRET_KEYnestrsstatic credentials
BUCKETnestrstarget bucket
FORCE_PATH_STYLEtruetrueendpoint/bucket/key; false ⇒ virtual-hosted

The canonical flow keeps bytes off the API. A handler injects Storage and returns a signed PUT URL; the client uploads straight to the object store; a later call reads the object’s metadata to finalize.

src/media/service.rs
use std::sync::Arc;
use std::time::Duration;
use nest_rs_core::injectable;
use nest_rs_storage::{Result, Storage};
#[injectable]
pub struct MediaService {
#[inject]
storage: Arc<Storage>,
}
impl MediaService {
/// Hand the client a short-lived URL to PUT the cover image directly.
pub async fn request_cover_upload(&self, key: &str) -> Result<String> {
self.storage.presign_put(key, Duration::from_secs(900)).await
}
/// After the client uploads, read the size back to finalize the record.
/// `None` ⇒ the client never completed the upload.
pub async fn confirm_cover_upload(&self, key: &str) -> Result<Option<i64>> {
Ok(self.storage.head(key).await?.map(|info| info.byte_size))
}
}

Workers that transform objects (e.g. generating a WebP variant) read and write bytes directly:

let original = self.storage.get_bytes(key).await?;
let webp = transcode(&original)?;
self.storage
.put_bytes(&variant_key, webp, "image/webp")
.await?;

presign_get is the counterpart to presign_put for serving private originals: a short-lived signed GET URL the client fetches directly.

MethodPurpose
bucket_name()the configured bucket
presign_put(key, expires)signed PUT URL for direct client upload
presign_get(key, expires)signed GET URL for serving private originals
head(key) -> Option<HeadMetadata>object size; None if absent
get_bytes(key)download full bytes (server-side)
put_bytes(key, bytes, content_type)upload bytes (server-side)

Because the seam is the object_store traits, pointing Storage at GCS, Azure, the local filesystem, or in-memory is a builder change inside the crate — not an API change for the features that inject it. The S3 driver is the default because it also covers every S3-compatible server.

  • Queue — background workers that read and transform stored objects.
  • Database — persist the record a presigned upload finalizes.
  • Configuration — the dual-path rule behind StorageConfig.
  • crates/nest-rs-storage/Storage, HeadMetadata, StorageConfig, StorageModule.
  • audio in the demo — presigns an upload URL, and the worker reads the stored object and writes a derived one.

Built by YV17labs