Update User Settings
Sparse update of preferences (locale, theme, notification flags, digest mode). Locale changes also sync to the legacy User.locale field.
PATCH /api/v1/users/settings — 🔑 Bearer
Sparse update — only fields present in the body are touched. If the user has no UserSettings row yet, one is created with the submitted values (and Prisma defaults filling the rest). When locale changes, the same value is also written back to the legacy User.locale field for backward compatibility.
UserSettings.locale is the source of truth. User.locale exists as a deprecated mirror for legacy code paths and is kept in sync by this endpoint when you patch locale. Frontend should always read locale from /users/settings, not /users/profile.
Request
Body — UpdateSettingsDto
All fields optional. Send only what you want to change.
| Field | Type | Validation | Notes |
|---|---|---|---|
locale | string | MaxLength(10) | e.g. en, tr, pt-BR |
theme | string | MaxLength(20) | e.g. dark, light, system |
emailNotifications | boolean | — | |
pushNotifications | boolean | — | |
inAppNotifications | boolean | — | |
digestMode | string | IsIn(['DAILY','WEEKLY','NONE']) |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | ✓ | JWT from POST /auth/login |
Response
200 OK — ApiResponseOf<UserSettingsDto>
Returns the fresh settings row after update (same shape as GET /users/settings):
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"userId": "f0e1d2c3-b4a5-6789-0123-456789abcdef",
"locale": "tr",
"theme": "dark",
"emailNotifications": true,
"pushNotifications": false,
"inAppNotifications": true,
"digestMode": "WEEKLY",
"createdAt": "2026-04-29T08:30:00.000Z",
"updatedAt": "2026-04-29T20:00:00.000Z"
}
}Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
400 | (DTO validation) | Invalid digestMode enum, fields exceeding length |
401 | (guard) | Missing / invalid bearer token |
Side effects
- Lookup existing
UserSettingsrow (may benull). - Build sparse
dataobject from defined keys only. - If no fields supplied, return the existing row unchanged (no DB write).
- Inside one transaction:
- If row exists:
prisma.userSettings.update({ where: { userId }, data }). - If missing:
prisma.userSettings.create({ data: { id: randomUUID(), userId, ...data } }). - If
localewas supplied: alsoprisma.user.update({ where: { id: userId }, data: { locale } })(legacy mirror).
- If row exists:
- Audit log:
[settings] Updated for user {userId}. - Return a fresh
findUniqueof the updated row — guarantees the response reflects the post-write state.
Code samples
curl -X PATCH https://api.bio.re/api/v1/users/settings \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"locale": "tr",
"pushNotifications": false,
"digestMode": "WEEKLY"
}'type UpdateSettingsInput = {
locale?: string;
theme?: string;
emailNotifications?: boolean;
pushNotifications?: boolean;
inAppNotifications?: boolean;
digestMode?: 'DAILY' | 'WEEKLY' | 'NONE';
};
async function updateSettings(accessToken: string, input: UpdateSettingsInput): Promise<UserSettings> {
const res = await fetch('https://api.bio.re/api/v1/users/settings', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Update settings failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useUpdateSettings() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (input: UpdateSettingsInput) => {
const res = await fetch('/api/v1/users/settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Update settings failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as UserSettings;
},
onSuccess: (data) => {
// Write-through cache: settings is the canonical source, profile mirrors locale
qc.setQueryData(['users', 'settings'], data);
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
application/json
curl -X PATCH "https://loading/api/v1/users/settings" \ -H "Content-Type: application/json" \ -d '{}'{
"success": true,
"data": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b",
"locale": "en",
"theme": "dark",
"emailNotifications": true,
"pushNotifications": true,
"inAppNotifications": true,
"digestMode": "DAILY",
"createdAt": "2019-08-24T14:15:22Z",
"updatedAt": "2019-08-24T14:15:22Z"
}
}{
"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"
}
}{
"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 | 147–154 (updateSettings) |
| DTO (request) | apps/api-core/src/modules/user/dto/index.ts | 51–69 (UpdateSettingsDto) |
| DTO (response) | apps/api-core/src/modules/user/dto/user-client-response.dto.ts | 134–164 (UserSettingsDto) |
| Service | apps/api-core/src/modules/user/user.service.ts | 362–392 (updateSettings) |
| Prisma models | packages/prisma/prisma/schema.prisma | UserSettings, User.locale (legacy mirror) |