Enterprise-backend-API's met NestJS · Les

Rechtstreekse S3-uploads met presigned URL's

Verplaats zware uploads naar object storage door kortlevende presigned URL's uit te geven vanuit de API.

Les 3 van 413 stappen

Rechtstreekse S3-uploads met presigned URL's is een gratis Enterprise-backend-API's met NestJS-les op CoddyKit. Dit is les 3 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Enterprise-backend-API's met NestJS. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Enterprise-backend-API's met NestJS bevat in totaal 4 lessen.

Waarom uploads niet via de API doorsturen?

Wanneer een client een groot bestand uploadt, stuurt het naïeve ontwerp de bytes naar uw NestJS-API, die ze vervolgens doorstuurt naar objectopslag. Hierdoor wordt uw server een flessenhals.

  • Druk op geheugen en CPU — elke upload gebruikt een verzoekthread en wordt door uw proces gebufferd of gestreamd.
  • Dubbele bandbreedte — bytes reizen van client → API → S3, dus u betaalt twee keer voor dezelfde gegevens.
  • Verzoeken verlopen — load balancers (bijvoorbeeld ALB, Nginx) beperken de duur van verzoeken; uploads van meerdere GB lopen vast.

De oplossing: laat de browser rechtstreeks naar S3 uploaden. Uw API geeft alleen een kort geldige, ondertekende URL uit die toestemming geeft voor één specifieke bewerking.

Wat is een vooraf ondertekende URL?

Een vooraf ondertekende URL is een normale S3-object-URL met extra queryparameters die een tijdelijke, cryptografisch ondertekende toestemming bevatten. Iedereen die de URL bezit, kan precies één bewerking (bijvoorbeeld PutObject) uitvoeren op precies één sleutel, totdat de URL verloopt.

  • Ondertekend met uw AWS-inloggegevens, maar de inloggegevens worden nooit blootgesteld — alleen de handtekening wordt meegestuurd.
  • Beperkt tot één HTTP-methode, bucket en objectsleutel.
  • Heeft een vaste verlooptijd (in seconden), waarna S3 de URL afwijst met 403.

Omdat S3 de handtekening zelf valideert, verwerkt uw API de bestandsbytes helemaal niet.

De uploadstroom

De volledige stroom heeft drie deelnemers: de browser, uw NestJS-API en S3.

  • 1. Aanvragen: de browser vraagt de API: "Ik wil avatar.png uploaden, 240 KB, image/png."
  • 2. Ondertekenen: de API valideert het verzoek, genereert een unieke sleutel en retourneert een vooraf ondertekende PUT-URL.
  • 3. Uploaden: de browser voert een PUT uit met de onbewerkte bytes rechtstreeks naar die URL op S3.
  • 4. Bevestigen: de browser meldt de API dat de upload is geslaagd; de API slaat de sleutel op in de database.

Uw API blijft snel en stateless — deze stuurt de payload nooit door.

De S3-client configureren

Gebruik de modulaire pakketten van AWS SDK v3. Maak één S3Client-instantie en deel deze via een NestJS-provider, zodat inloggegevens en regio op één plek worden geconfigureerd.

Inloggegevens komen uit omgevingsvariabelen (of, in productie, uit een IAM-rol). Neem ze nooit rechtstreeks op in de code.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3Client } from '@aws-sdk/client-s3';

@Injectable()
export class S3Provider {
  readonly client: S3Client;
  readonly bucket: string;

  constructor(private readonly config: ConfigService) {
    this.bucket = config.getOrThrow<string>('S3_BUCKET');
    this.client = new S3Client({
      region: config.getOrThrow<string>('AWS_REGION'),
      credentials: {
        accessKeyId: config.getOrThrow<string>('AWS_ACCESS_KEY_ID'),
        secretAccessKey: config.getOrThrow<string>('AWS_SECRET_ACCESS_KEY'),
      },
    });
  }
}

Een vooraf ondertekende PUT-URL genereren

Het pakket @aws-sdk/s3-request-presigner ondertekent een opdracht zonder deze uit te voeren. U bouwt een PutObjectCommand die de doelsleutel en het inhoudstype beschrijft en roept vervolgens getSignedUrl aan met een expiresIn.

  • ContentType in de opdracht wordt afgedwongen: de browser moet een overeenkomende Content-Type-header verzenden.
  • expiresIn wordt uitgedrukt in seconden — houd deze waarde kort (60–300 seconden), zodat gelekte URL's snel ongeldig worden.
import { Injectable } from '@nestjs/common';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3Provider } from './s3.provider';

@Injectable()
export class UploadsService {
  constructor(private readonly s3: S3Provider) {}

  async createUploadUrl(key: string, contentType: string): Promise<string> {
    const command = new PutObjectCommand({
      Bucket: this.s3.bucket,
      Key: key,
      ContentType: contentType,
    });
    return getSignedUrl(this.s3.client, command, { expiresIn: 120 });
  }
}

Veilige, unieke objectsleutels genereren

Vertrouw nooit op de bestandsnaam van de client als S3-sleutel. Een gebruiker kan ../../etc/passwd verzenden of het bestand van een andere gebruiker overschrijven. Genereer de sleutel aan de serverkant.

  • Gebruik de eigenaar als naamruimte: uploads/{userId}/..., zodat autorisatie eenvoudig te begrijpen is.
  • Gebruik een willekeurige UUID om uniciteit te garanderen.
  • Behoud alleen een opgeschoonde extensie voor aanwijzingen over het inhoudstype en voor hulpmiddelen.
import { randomUUID } from 'node:crypto';
import { extname } from 'node:path';

function buildObjectKey(userId: string, originalName: string): string {
  const ext = extname(originalName).toLowerCase().replace(/[^.a-z0-9]/g, '');
  const safeExt = /^\.[a-z0-9]{1,8}$/.test(ext) ? ext : '';
  return `uploads/${userId}/${randomUUID()}${safeExt}`;
}

console.log(buildObjectKey('user-42', 'My Vacation Photo.PNG'));
console.log(buildObjectKey('user-42', 'sneaky/../../etc/passwd'));

Het controller-eindpunt

Stel een beveiligd eindpunt beschikbaar dat de metagegevens van het bestand ontvangt, deze met een DTO valideert en de vooraf ondertekende URL plus de uiteindelijke sleutel retourneert. De client heeft de sleutel later nodig om de upload te bevestigen of de openbare/lees-URL op te bouwen.

Authenticatie is hier belangrijk: het ondertekeningseindpunt is uw toegangspoort. S3 vertrouwt elke geldige handtekening, dus alle controles (wie, welke grootte, welk type) moeten plaatsvinden voordat u ondertekent.

import { Body, Controller, Post, UseGuards, Req } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { UploadsService } from './uploads.service';
import { CreateUploadDto } from './dto/create-upload.dto';

@UseGuards(JwtAuthGuard)
@Controller('uploads')
export class UploadsController {
  constructor(private readonly uploads: UploadsService) {}

  @Post('presign')
  async presign(@Req() req, @Body() dto: CreateUploadDto) {
    return this.uploads.presignForUser(req.user.id, dto);
  }
}

Het DTO voor het uploadverzoek valideren

Valideer de metagegevens voordat u ondertekent. Weiger niet-toegestane MIME-typen en te grote bestanden op API-niveau — maar onthoud dat de client kan liegen, dus dit is een eerste verdedigingslinie, niet de laatste.

  • Vergelijk contentType met een lijst met toegestane waarden.
  • Beperk de opgegeven size om duidelijk enorme uploads snel af te wijzen.
import { IsIn, IsInt, IsString, Max, Min } from 'class-validator';

const ALLOWED = ['image/png', 'image/jpeg', 'image/webp', 'application/pdf'] as const;

export class CreateUploadDto {
  @IsString()
  filename: string;

  @IsIn(ALLOWED)
  contentType: (typeof ALLOWED)[number];

  @IsInt()
  @Min(1)
  @Max(10 * 1024 * 1024) // 10 MB
  size: number;
}

Grootte afdwingen met een ondertekende Content-Length

De controle van de DTO-grootte is slechts adviserend — de browser bepaalt nog steeds hoeveel bytes deze daadwerkelijk PUT. Om S3 zelf te laten weigeren dat uploads te groot zijn, bindt u een bereik voor content-length aan de handtekening.

Onderteken voor één PUT ContentLength, zodat S3 een exact aantal bytes afdwingt. Gebruik voor flexibelere limieten (een minimum-/maximum bereik) in plaats daarvan een vooraf ondertekend POST-beleid, dat voorwaarden met content-length-range ondersteunt.

import { createPresignedPost } from '@aws-sdk/s3-presigned-post';

async function presignPost(client, bucket: string, key: string) {
  return createPresignedPost(client, {
    Bucket: bucket,
    Key: key,
    Conditions: [
      ['content-length-range', 1, 10 * 1024 * 1024], // 1 byte – 10 MB
      ['starts-with', '$Content-Type', 'image/'],
    ],
    Fields: { 'Content-Type': 'image/png' },
    Expires: 120,
  });
}

De upload vanuit de browser

Met de vooraf ondertekende PUT-URL bij de hand uploadt de browser met een gewone fetch. Er is geen SDK en geen AWS-inloggegevens op de client — alleen de bytes en een overeenkomende Content-Type.

  • De header moet gelijk zijn aan de ContentType die u hebt ondertekend, anders retourneert S3 403 SignatureDoesNotMatch.
  • Verzend geen Authorization — de handtekening staat in de querystring.
async function uploadFile(presignedUrl: string, file: File): Promise<void> {
  const res = await fetch(presignedUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file,
  });
  if (!res.ok) {
    throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
  }
}

De upload bevestigen en uitlezen

Omdat S3 uw API niet informeert, belt de client na een geslaagde PUT terug, zodat u de sleutel kunt opslaan. Voor extra veiligheid kan de API HeadObject uitvoeren om te controleren of het object echt bestaat en de werkelijke grootte en het type te controleren voordat u het vertrouwt.

Om het bestand later aan te bieden, houdt u de bucket privé en geeft u op verzoek een vooraf ondertekende GET-URL uit, of slaat u voor openbare middelen de openbare URL op en retourneert u deze.

import { GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

async confirm(userId: string, key: string) {
  const head = await this.s3.client.send(
    new HeadObjectCommand({ Bucket: this.s3.bucket, Key: key }),
  );
  if ((head.ContentLength ?? 0) > 10 * 1024 * 1024) {
    throw new BadRequestException('Object exceeds size limit');
  }
  await this.files.save({ userId, key, size: head.ContentLength });
}

async downloadUrl(key: string) {
  return getSignedUrl(
    this.s3.client,
    new GetObjectCommand({ Bucket: this.s3.bucket, Key: key }),
    { expiresIn: 300 },
  );
}

Korte controle

Test uw begrip van waar de toegangscontrole zich in dit patroon bevindt.

Samenvatting

U hebt geleerd hoe u zware uploads naar S3 verplaatst met vooraf ondertekende URL's:

  • Waarom: bytes via de API doorsturen verspilt bandbreedte en geheugen en leidt tot verlopen verzoeken.
  • Hoe: de API ondertekent een kort geldige PutObjectCommand met getSignedUrl; de browser voert rechtstreeks een PUT naar S3 uit.
  • Sleutels: genereer deze altijd aan de serverkant (UUID + naamruimte per gebruiker); vertrouw nooit op bestandsnamen van de client.
  • Beveiliging: het eindpunt voor vooraf ondertekenen is de toegangspoort — verifieer de identiteit, sta MIME-typen toe via een lijst en beperk daar de grootte. Gebruik content-length-range bij vooraf ondertekende POST's om S3 de grootte te laten afdwingen.
  • Na de upload: bevestig met HeadObject, sla de sleutel op en bied het bestand later aan via vooraf ondertekende URL's voor GetObjectCommand.
Gratis beginnen

Leer TypeScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
20
Lessen
76

Veelgestelde vragen

Is de les “Rechtstreekse S3-uploads met presigned URL's” gratis?

Ja — de volledige tekst van “Rechtstreekse S3-uploads met presigned URL's” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Enterprise-backend-API's met NestJS wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Enterprise-backend-API's met NestJS bevat in totaal 4 lessen.

Wat leer ik in “Rechtstreekse S3-uploads met presigned URL's”?

Verplaats zware uploads naar object storage door kortlevende presigned URL's uit te geven vanuit de API. Je oefent met Enterprise-backend-API's met NestJS door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Enterprise-backend-API's met NestJS te beginnen?

Ervaring vooraf is niet nodig. Enterprise-backend-API's met NestJS op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “Rechtstreekse S3-uploads met presigned URL's”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Enterprise-backend-API's met NestJS?

Ja. Elke les over Enterprise-backend-API's met NestJS bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Multipartuploads met Multer-interceptors
  2. Grote responses streamen met StreamableFile
  3. Rechtstreekse S3-uploads met presigned URL's
  4. Pijplijnen voor beeldverwerking met Sharp
← Terug naar Enterprise-backend-API's met NestJS