Aviary
Recipes

Serving files

Public URLs, signed temporary URLs, and streaming files through a controller.

Three ways to get bytes back to a client, depending on whether the file is public, private, or proxied through your app.

Public URLs

If the disk has a public base (CloudFront/CDN for S3, a static base for local), url() returns a stable address you can hand straight to the browser:

const src = await media.library.url(mediaId);           // original
const thumb = await media.library.url(mediaId, 'thumb'); // a conversion

For raw storage: media.disk('s3').url('path/to/file.png').

Signed, expiring URLs

For private files, hand out a short-lived signed URL instead — no proxying, but access expires:

const link = await media.disk('s3').temporaryUrl(`invoices/${id}.pdf`, 300); // 5 minutes

temporaryUrl needs a presign-capable disk (S3). The local disk doesn't sign — proxy those through a controller (below) or put a CDN with signed URLs in front.

Streaming through a controller

When you need auth checks, range support, or the disk can't sign, stream the bytes through Nest:

files.controller.ts
import { Controller, Get, Param, StreamableFile, Res } from '@nestjs/common';
import type { Response } from 'express';
import { MediaService } from '@dudousxd/nestjs-media';

@Controller('media')
export class FilesController {
  constructor(private readonly media: MediaService) {}

  @Get(':id/raw')
  async raw(@Param('id') id: string, @Res({ passthrough: true }) res: Response) {
    const record = (await this.media.library.list('Post', '...', '...')).find((m) => m.id === id);
    // …authorize here…
    res.set({ 'Content-Type': record!.mimeType });
    return new StreamableFile(await this.media.disk(record!.disk).stream(record!.path));
  }
}

Prefer stream over get for serving — it pipes without loading the whole file into memory, which matters for video and large documents.

On this page