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
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
avatarUrl | string (URL) | ✓ | IsUrl(), MaxLength(2048) | CDN URL of the uploaded image |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<AvatarUrlResponseDto>
{
"success": true,
"data": {
"avatarUrl": "https://cdn.bio.re/avatars/user/uuid.webp"
}
}| Field | Type | Notes |
|---|---|---|
avatarUrl | string | Echoed back — same URL you sent (server does not transform it) |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | avatarUrl not a valid URL OR longer than 2048 chars |
401 | (guard) | Missing / invalid bearer token |
Side effects
- Read current
User.avatarUrl(to remember which file to delete). prisma.user.update({ avatarUrl }).- 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).
- 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
Authorization
bearer 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
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/user/user.controller.ts | 73–79 (setAvatar) |
| DTO (request) | apps/api-core/src/modules/user/dto/index.ts | 5–7 (SetAvatarDto) |
| DTO (response) | apps/api-core/src/modules/user/dto/user-client-response.dto.ts | 113–116 (AvatarUrlResponseDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 90–112 (setAvatar) |
| Object storage cleanup | apps/api-core/src/modules/upload/upload.service.ts | extractKeyFromUrl(), deleteFile() |
| Prisma model | packages/prisma/prisma/schema.prisma | User.avatarUrl |