BIO.RE
User

Set Avatar

Persist a new avatar URL on the user row and clean up the previous file from object storage in the background.

PATCH /api/v1/users/avatar — 🔑 Bearer

Persists a new avatar URL on User.avatarUrl and deletes the previous file from object storage in the background (fire-and-forget, no error surfaced if the old file is already gone).

This endpoint does not upload — it only persists the URL after upload. To upload the image bytes, use the upload module first to get a CDN URL, then send that URL here. The cleanup of the previous file is the reason to prefer this over PATCH /users/profile { avatarUrl }.

The previous file deletion happens after the response is returned. If the old file is referenced elsewhere (e.g., cached in another row), reflect that before calling this endpoint.

Request

Body — SetAvatarDto

FieldTypeRequiredValidationNotes
avatarUrlstring (URL)IsUrl(), MaxLength(2048)CDN URL of the uploaded image
HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

200 OKApiResponseOf<AvatarUrlResponseDto>

{
  "success": true,
  "data": {
    "avatarUrl": "https://cdn.bio.re/avatars/user/uuid.webp"
  }
}
FieldTypeNotes
avatarUrlstringEchoed back — same URL you sent (server does not transform it)

Errors

HTTPcode / i18nKeyReason
400(DTO validation)avatarUrl not a valid URL OR longer than 2048 chars
401(guard)Missing / invalid bearer token

Side effects

  1. Read current User.avatarUrl (to remember which file to delete).
  2. prisma.user.update({ avatarUrl }).
  3. Fire-and-forget: extract object-storage key from the old URL and delete it via the upload service. Failures are logged, not surfaced to the caller (so a stale orphan never breaks the avatar update path).
  4. Audit log: [avatar] Updated for user {userId}.

Code samples

curl -X PATCH https://api.bio.re/api/v1/users/avatar \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"avatarUrl": "https://cdn.bio.re/avatars/user/abc.webp"}'
type AvatarUrlResponse = { avatarUrl: string };

async function setAvatar(accessToken: string, avatarUrl: string): Promise<AvatarUrlResponse> {
  const res = await fetch('https://api.bio.re/api/v1/users/avatar', {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ avatarUrl }),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Set avatar failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useSetAvatar() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (avatarUrl: string) => {
      const res = await fetch('/api/v1/users/avatar', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ avatarUrl }),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Set avatar failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as AvatarUrlResponse;
    },
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['users', 'profile'] });
      qc.invalidateQueries({ queryKey: ['auth', 'me'] });
    },
  });
}

Try it

PATCH
/api/v1/users/avatar
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

curl -X PATCH "https://loading/api/v1/users/avatar" \  -H "Content-Type: application/json" \  -d '{    "avatarUrl": "http://example.com"  }'
{
  "success": true,
  "data": {
    "avatarUrl": "https://cdn.bio.re/avatars/user/uuid.webp"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/user/user.controller.ts73–79 (setAvatar)
DTO (request)apps/api-core/src/modules/user/dto/index.ts5–7 (SetAvatarDto)
DTO (response)apps/api-core/src/modules/user/dto/user-client-response.dto.ts113–116 (AvatarUrlResponseDto)
Serviceapps/api-core/src/modules/user/user.service.ts90–112 (setAvatar)
Object storage cleanupapps/api-core/src/modules/upload/upload.service.tsextractKeyFromUrl(), deleteFile()
Prisma modelpackages/prisma/prisma/schema.prismaUser.avatarUrl

On this page