Allmine API

Users search v2 — Expo RTK Query

~5 dkMobil / WebKararlı

GET /api/v2/users/search Expo entegrasyonu

Users Search V2 - Expo (RTK Query) Entegrasyon Dokumani

Migration rehberi

Yapılandırma sırası için Migration şablonu. Yeni düzenlemelerde Amaç → Önkoşullar → Endpoint → Request/Response → Hata kodları → Client adımları → İlgili sayfalar bölümlerini tercih edin.

Bu doküman, GET /api/v2/users/search endpointini Expo projesinde Redux Toolkit Query (RTK Query) ile entegre etmek için hazırlandı.

1) Endpoint Ozeti

  • URL: GET /api/v2/users/search
  • Auth: Bearer <JWT> zorunlu
  • Query Params:
    • q?: string
    • page?: number (default: 1)
    • limit?: number (default: 5, max: 100)
    • expertise?: Expertise

2) Response Sozlesmesi (V2)

Not: profilePhoto artık dönmez. UI tarafında profilePhoto96Url kullanılmalıdır.

export type Expertise =
  | 'musician'
  | 'trainer'
  | 'chef'
  | 'photographer'
  | 'designer'
  | 'stylist'
  | 'gamer'
  | 'educator'
  | 'coach'
  | 'entertainer'
  | 'developer'
  | 'creator';

export type LiveStreamResponseDto = {
  id: string;
  title: string;
  status: string;
  channelName: string;
  startedAt?: string | null;
  createdAt: string;
  updatedAt: string;
};

export type SearchUserV2Dto = {
  _id: string;
  username?: string;
  name?: string | null;
  surname?: string | null;
  minutePriceInLiveStream?: number | null;
  expertise?: Expertise | null;
  profilePhoto96Url?: string | null;
  activeLiveStream?: LiveStreamResponseDto | null;
};

export type PaginationDto = {
  currentPage: number;
  totalPages: number;
  totalItems: number;
  itemsPerPage: number;
  hasNextPage?: boolean;
  hasPrevPage?: boolean;
};

export type PaginatedResponseDto<T> = {
  list: T[];
  pagination: PaginationDto;
};

export type BaseResponseDto<T> = {
  isSuccess: boolean;
  statusCode: number;
  data: T;
  errors?: string[];
  timestamp: string;
};

3) RTK Query - Base API

src/store/api/baseApi.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { RootState } from '../store';

const API_URL = process.env.EXPO_PUBLIC_API_URL;

export const baseApi = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: `${API_URL}/api`,
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.accessToken;
      if (token) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ['UsersSearchV2'],
  endpoints: () => ({}),
});

4) RTK Query - Users Search V2 Endpoint

src/store/api/usersSearchV2Api.ts

import { baseApi } from './baseApi';
import type {
  BaseResponseDto,
  Expertise,
  PaginatedResponseDto,
  SearchUserV2Dto,
} from '@/types/users-search-v2';

export type SearchUsersV2QueryParams = {
  q?: string;
  page?: number;
  limit?: number;
  expertise?: Expertise;
};

export const usersSearchV2Api = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    searchUsersV2: builder.query<
      PaginatedResponseDto<SearchUserV2Dto>,
      SearchUsersV2QueryParams
    >({
      query: ({ q = '', page = 1, limit = 5, expertise }) => ({
        url: 'v2/users/search',
        params: {
          q,
          page,
          limit,
          ...(expertise ? { expertise } : {}),
        },
      }),
      transformResponse: (
        response: BaseResponseDto<PaginatedResponseDto<SearchUserV2Dto>>,
      ) => response.data,

      // Sonsuz liste (infinite scroll) için sayfalari tek cache key'de birlestir
      serializeQueryArgs: ({ endpointName, queryArgs }) => {
        const normalizedQ = (queryArgs.q ?? '').trim().toLowerCase();
        const expertise = queryArgs.expertise ?? 'all';
        const limit = queryArgs.limit ?? 5;
        return `${endpointName}-${normalizedQ}-${expertise}-${limit}`;
      },
      merge: (currentCache, newData, { arg }) => {
        const page = arg.page ?? 1;

        // Ilk sayfada cache'i yenile
        if (page === 1) {
          currentCache.list = newData.list;
          currentCache.pagination = newData.pagination;
          return;
        }

        // Sonraki sayfalarda duplicate id engelleyerek ekle
        const existingIds = new Set(currentCache.list.map((u) => u._id));
        const toAppend = newData.list.filter((u) => !existingIds.has(u._id));
        currentCache.list.push(...toAppend);
        currentCache.pagination = newData.pagination;
      },
      forceRefetch({ currentArg, previousArg }) {
        return (
          (currentArg?.q ?? '').trim() !== (previousArg?.q ?? '').trim() ||
          currentArg?.expertise !== previousArg?.expertise ||
          (currentArg?.limit ?? 5) !== (previousArg?.limit ?? 5)
        );
      },

      providesTags: (result) =>
        result
          ? [
              ...result.list.map((u) => ({
                type: 'UsersSearchV2' as const,
                id: u._id,
              })),
              { type: 'UsersSearchV2' as const, id: 'LIST' },
            ]
          : [{ type: 'UsersSearchV2' as const, id: 'LIST' }],
    }),
  }),
});

export const { useSearchUsersV2Query } = usersSearchV2Api;

5) Expo Screen Kullanim Ornegi

src/screens/SearchUsersScreen.tsx

import React, { useEffect, useMemo, useState } from 'react';
import { FlatList, Text, View } from 'react-native';
import { useSearchUsersV2Query } from '@/store/api/usersSearchV2Api';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';

export function SearchUsersScreen() {
  const [searchText, setSearchText] = useState('');
  const [selectedExpertise, setSelectedExpertise] = useState<string | undefined>(undefined);
  const [page, setPage] = useState(1);

  const debouncedQuery = useDebouncedValue(searchText, 350);

  // Query veya filtre degisince ilk sayfaya don
  useEffect(() => {
    setPage(1);
  }, [debouncedQuery, selectedExpertise]);

  const { data, isLoading, isFetching, error, refetch } = useSearchUsersV2Query({
    q: debouncedQuery,
    page,
    limit: 10,
    expertise: selectedExpertise as any,
  });

  const users = data?.list ?? [];
  const hasNextPage = data?.pagination?.hasNextPage ?? false;

  const onEndReached = () => {
    if (!isFetching && hasNextPage) {
      setPage((prev) => prev + 1);
    }
  };

  const emptyText = useMemo(() => {
    if (isLoading || isFetching) return 'Yukleniyor...';
    if (!users.length) return 'Sonuc bulunamadi';
    return '';
  }, [isLoading, isFetching, users.length]);

  return (
    <View style={{ flex: 1 }}>
      {/* Search input ve filter UI burada */}

      {!!error && <Text>Arama sirasinda hata olustu.</Text>}

      <FlatList
        data={users}
        keyExtractor={(item) => item._id}
        onEndReachedThreshold={0.4}
        onEndReached={onEndReached}
        onRefresh={refetch}
        refreshing={isFetching && page === 1}
        ListEmptyComponent={<Text>{emptyText}</Text>}
        renderItem={({ item }) => (
          <View style={{ padding: 12 }}>
            <Text>{item.username ?? '-'}</Text>
            <Text>{item.profilePhoto96Url ?? 'no-photo'}</Text>
            <Text>
              {item.activeLiveStream ? 'Yayinda' : 'Yayinda değil'}
            </Text>
          </View>
        )}
      />
    </View>
  );
}

6) Error Handling (RTK Query)

import type { FetchBaseQueryError } from '@reduxjs/toolkit/query';
import type { SerializedError } from '@reduxjs/toolkit';

type ApiErrorBody = {
  isSuccess?: boolean;
  statusCode?: number;
  errors?: string[];
  timestamp?: string;
};

export function getApiErrorMessage(
  error: FetchBaseQueryError | SerializedError | undefined,
): string {
  if (!error) return 'Bilinmeyen hata';

  if ('status' in error) {
    const data = error.data as ApiErrorBody | undefined;
    if (data?.errors?.length) return data.errors[0];
    if (typeof error.status === 'number') return `HTTP ${error.status}`;
    return 'Istek hatasi';
  }

  return error.message ?? 'Bilinmeyen hata';
}

7) Entegrasyon Kontrol Listesi

  1. baseUrl değeri /api prefixi icermeli.
  2. v2 endpoint çağrısı: v2/users/search.
  3. Auth header: Authorization: Bearer <token>.
  4. UI profilePhoto değil profilePhoto96Url kullanmali.
  5. Infinite scroll kullaniyorsaniz serializeQueryArgs + merge aktif olmali.
  6. Filtre degisiminde page=1 resetlenmeli.

8) Ornek Raw Response

{
  "isSuccess": true,
  "statusCode": 200,
  "data": {
    "list": [
      {
        "_id": "698b3452cddbd8c812e1cd9f",
        "username": "fgfg",
        "name": "Gf",
        "surname": "Fgf",
        "minutePriceInLiveStream": 10,
        "expertise": "trainer",
        "profilePhoto96Url": "https://s.allminelive.com/profiles/.../96.webp",
        "activeLiveStream": null
      }
    ],
    "pagination": {
      "currentPage": 1,
      "totalPages": 1,
      "totalItems": 1,
      "itemsPerPage": 5,
      "hasNextPage": false,
      "hasPrevPage": false
    }
  },
  "errors": [],
  "timestamp": "2026-02-20T08:38:51.978Z"
}

On this page