Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Windows Tipps & SecurityGarmin Cirqa im Test: Fitness-Tracker ohne Display(22.09.2026 um 10:30 Uhr)
Windows Tipps & SecuritySicher online bezahlen: 7 Methoden im Praxis-Check(22.09.2026 um 09:00 Uhr)
Sichere Programmierunghas_tokens: true is a boolean. 476 of 791 have no market behind them.(22.09.2026 um 10:00 Uhr)
Sichere ProgrammierungClaude Code Hooks – Safety Through Invariants(22.09.2026 um 10:04 Uhr)
Sichere ProgrammierungYour MCP Tool Just Returned a Secret. Did It Need To?(22.09.2026 um 10:05 Uhr)
Windows Tipps & SecurityGarmin Cirqa im Test: Fitness-Tracker ohne Display(22.09.2026 um 10:30 Uhr)
Windows Tipps & SecuritySicher online bezahlen: 7 Methoden im Praxis-Check(22.09.2026 um 09:00 Uhr)
Sichere Programmierunghas_tokens: true is a boolean. 476 of 791 have no market behind them.(22.09.2026 um 10:00 Uhr)
Sichere ProgrammierungClaude Code Hooks – Safety Through Invariants(22.09.2026 um 10:04 Uhr)
Sichere ProgrammierungYour MCP Tool Just Returned a Secret. Did It Need To?(22.09.2026 um 10:05 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Next.js API Management: Implement Search, Pagination, Filter, Sort, and Limit Features

In this article, I want to share how I handle APIs in a Next.js application, including fetching data, displaying it, and implementing features such as searching, pagination, filtering, sorting, and limiting results. Let's dive straight…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

In this article, I want to share how I handle APIs in a Next.js application, including fetching data, displaying it, and implementing features such as searching, pagination, filtering, sorting, and limiting results. Let's dive straight into the details.






Requirements:




  • Next.js 14.2.18

  • Axios 1.7.8

  • Use debounce 10.0.4

  • API, Make sure the API you use supports features like searching, pagination, filtering, sorting, and limiting data. In this article, I'll demonstrate using the Jikan API, which provides data for anime and related content.






Project Setup




  1. Install Next.js and Required Packages

  2. Create the File and Folder Structure
    Organize your project with the following structure:



project structure






Fetch and Display Data






//src/lib/actions.ts

import axios from 'axios';

const axiosInstance = axios.create({
baseURL: 'https://api.jikan.moe/v4',
});

export const getAllAnime = async (
query?: string,
currentPage?: number,
orderBy?: string,
type?: string,
status?: string,
sort?: string,
limit?: number
) => {
try {
const res = await axiosInstance.get(
`/anime?q=${query}&page=${currentPage}&order_by=${orderBy}&type=${type}&status=${status}&sort=${sort}&limit=${limit}`
);

const totalPages = res.data.pagination.last_visible_page;

return {
data: res.data.data,
totalPages,
};
} catch (error) {
console.log(error);
return null;
}
};









//src/components/AnimeList.tsx

import { getAllAnime } from '@/lib/actions';
import Image from 'next/image';

type AnimeProps = {
mal_id: number;
images: {
webp: {
image_url: string;
};
};
title: string;
};

export default async function AnimeList({
query,
currentPage,
orderBy,
type,
status,
sort,
limit,
}: {
query?: string;
currentPage?: number;
orderBy?: string;
type?: string;
status?: string;
sort?: string;
limit?: number;
}) {
const animes = await getAllAnime(
query,
currentPage,
orderBy,
type,
status,
sort,
limit
);

return (
<>
<div className="grid grid-cols-6 gap-4 pt-4">
{animes?.data.map((anime: AnimeProps) => (
<div key={anime.mal_id}>
<Image
src={anime.images.webp.image_url}
alt={anime.title}
width={500}
height={500}
className="w-full h-[250px] object-cover rounded-md"
/>
<h3>
{anime.title.length > 15
? `${anime.title.slice(0, 15)}...`
: anime.title}
</h3>
</div>
))}
</div>
</>
);
}









Implement Search






//src/components/Search.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';

export default function Search() {
const pathname = usePathname();
const { replace } = useRouter();
const searchParams = useSearchParams();

const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}, 500);

return (
<div className="w-[50%]">
<input
type="text"
placeholder="Search..."
className="w-full bg-neutral-200 px-2 py-1 rounded-md"
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
</div>
);
}









Implement Pagination






//src/components/Pagination.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const router = useRouter();

const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
return `${pathname}?${params.toString()}`;
};

return (
<>
<div className="w-full flex items-center justify-center gap-x-8">
<button
onClick={() => router.push(createPageURL(currentPage - 1))}
disabled={currentPage === 1}
className="bg-neutral-600 px-2 py-1 rounded-md text-neutral-100 disabled:bg-neutral-400 disabled:cursor-not-allowed"
>
Prev
</button>
<p className="text-[1rem] text-neutral-700">
Page&nbsp;{currentPage} of&nbsp;{totalPages}
</p>
<button
onClick={() => router.push(createPageURL(currentPage + 1))}
disabled={currentPage === totalPages}
className="bg-neutral-600 px-2 py-1 rounded-md text-neutral-100 disabled:bg-neutral-400 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</>
);
}









Implement Filter and Sort






//src/components/FilterByType.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type TypeProps = { value: string; name: string };

const TYPE = [
{ value: 'tv', name: 'TV' },
{ value: 'movie', name: 'Movie' },
{ value: 'ova', name: 'OVA' },
{ value: 'special', name: 'Special' },
{ value: 'ona', name: 'ONA' },
{ value: 'music', name: 'Music' },
{ value: 'cm', name: 'CM' },
{ value: 'pv', name: 'PV' },
{ value: 'tv_special', name: 'TV Special' },
];

export default function FilterByType() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleFilterByType = (type: string) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
params.set('type', type);
router.replace(`${pathname}?${params.toString()}`);
};

return (
<>
<form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
<select
className="bg-neutral-400 w-full"
onChange={(e) => handleFilterByType(e.target.value)}
defaultValue={searchParams.get('type')?.toString()}
>
<option value={''} defaultValue={''}>
Type:
</option>
{TYPE.map((type: TypeProps, index) => (
<option key={index} value={type.value}>
{type.name}
</option>
))}
</select>
</form>
</>
);
}









//src/components/FilterByStatus.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type StatusProps = { value: string; name: string };

const STATUS = [
{ value: 'airing', name: 'Airing' },
{ value: 'upcoming', name: 'Upcoming' },
{ value: 'complete', name: 'Complete' },
];

export default function FilterByStatus() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleFilterByStatus = (status: string) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
params.set('status', status);
router.replace(`${pathname}?${params.toString()}`);
};
return (
<>
<form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
<select
className="bg-neutral-400 w-full"
onChange={(e) => handleFilterByStatus(e.target.value)}
defaultValue={searchParams.get('status')?.toString()}
>
<option value={''} defaultValue={''}>
Status:
</option>
{STATUS.map((status: StatusProps, index) => (
<option key={index} value={status.value}>
{status.name}
</option>
))}
</select>
</form>
</>
);
}









//src/components/SortByOrder

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type OrderByProps = { value: string; name: string };

const ORDER_BY = [
{ value: 'mal_id', name: 'Mal id' },
{ value: 'title', name: 'Title' },
{ value: 'start_date', name: 'Start date' },
{ value: 'end_date', name: 'End date' },
{ value: 'episodes', name: 'Episodes' },
{ value: 'score', name: 'Score' },
{ value: 'scored_by', name: 'Scored by' },
{ value: 'rank', name: 'Rank' },
{ value: 'popularity', name: 'Popularity' },
{ value: 'members', name: 'Members' },
{ value: 'favorites', name: 'Favorites' },
];

export default function SortByOrder() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleSortByOrder = (orderBy: string) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
params.set('order_by', orderBy);
router.replace(`${pathname}?${params.toString()}`);
};
return (
<>
<form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
<select
className="bg-neutral-400 w-full"
onChange={(e) => handleSortByOrder(e.target.value)}
defaultValue={searchParams.get('order_by')?.toString()}
>
<option value={''} defaultValue={''}>
Order by:
</option>
{ORDER_BY.map((orderBy: OrderByProps, index: number) => (
<option key={index} value={orderBy.value}>
{orderBy.name}
</option>
))}
</select>
</form>
</>
);
}









//src/components/SortDirection.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type SortDirectionProps = { value: string; name: string };

const SORT_DIRECTION = [
{ value: 'asc', name: 'Ascending' },
{ value: 'desc', name: 'Descending' },
];

export default function SortDirection() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleSortDirection = (sort: string) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
params.set('sort', sort);
router.replace(`${pathname}?${params.toString()}`);
};
return (
<>
<form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
<select
className="bg-neutral-400 w-full"
onChange={(e) => handleSortDirection(e.target.value)}
defaultValue={searchParams.get('sort')?.toString()}
>
<option value={''} defaultValue={''}>
Sort:
</option>
{SORT_DIRECTION.map((orderBy: SortDirectionProps, index: number) => (
<option key={index} value={orderBy.value}>
{orderBy.name}
</option>
))}
</select>
</form>
</>
);
}









Limiting Results






//src/components/Limit.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

export default function Limit() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleLimitChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const params = new URLSearchParams(searchParams);

if (!isNaN(Number(value)) && Number(value) > 0) {
params.set('page', '1');
params.set('limit', value);
router.replace(`${pathname}?${params.toString()}`);
}

if (
Number(value) === 0 ||
isNaN(Number(value)) ||
value === '' ||
Number(value) > 25
) {
params.delete('limit');
router.replace(`${pathname}?${params.toString()}`);
}
};

return (
<form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md flex items-center gap-x-2">
<label htmlFor="limit">Limit:</label>
<input
id="limit"
type="string"
className="bg-neutral-400 w-full placeholder-neutral-700"
min="1"
placeholder="1-25"
defaultValue={searchParams.get('limit') || ''}
onChange={handleLimitChange}
/>
</form>
);
}









Home Page






//src/app/page

import AnimeList from '@/components/AnimeList';
import FilterByStatus from '@/components/FilterByStatus';
import FilterByType from '@/components/FilterByType';
import Limit from '@/components/Limit';
import Pagination from '@/components/Pagination';
import Search from '@/components/Search';
import SortByOrder from '@/components/SortByOrder';
import SortDirection from '@/components/SortDirection';
import { getAllAnime } from '@/lib/actions';
import { Suspense } from 'react';

export default async function Home({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
order_by?: string;
type?: string;
status?: string;
sort?: string;
limit?: string;
};
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
const orderBy = searchParams?.order_by || '';
const type = searchParams?.type || '';
const status = searchParams?.status || '';
const sort = searchParams?.sort || '';
const limit = Number(searchParams?.limit) || 25;

const pages = await getAllAnime(
query,
currentPage,
orderBy,
type,
status,
sort,
limit
);
const totalPages = pages?.totalPages;

return (
<main className="w-[1020px] mx-auto">
<div className="flex items-center gap-x-4 py-4">
<Search />
<FilterByType />
<FilterByStatus />
<SortByOrder />
<SortDirection />
<Limit />
</div>
<Pagination totalPages={totalPages} />
<Suspense
key={query + currentPage + orderBy + type + status + sort + limit}
fallback={<div>Loading...</div>}
>
<AnimeList
query={query}
currentPage={currentPage}
orderBy={orderBy}
type={type}
status={status}
sort={sort}
limit={limit}
/>
</Suspense>
</main>
);
}









Refactor Components



To make the code more concise and reusable, we can refactor the filter and sort components to reduce redundancy. Use FilterSelect.tsx component that can handle both the filtering and sorting functionality. Here's how you can refactor your components:




//src/components/FilterSelect.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';

type FilterSelectProps = {
options: { value: string; name: string }[];
paramKey: string;
label: string;
};

const FilterSelect: React.FC<FilterSelectProps> = ({
options,
paramKey,
label,
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleChange = (value: string) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
params.set(paramKey, value);
router.replace(`${pathname}?${params.toString()}`);
};

return (
<form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
<select
className="bg-neutral-400 w-full"
onChange={(e) => handleChange(e.target.value)}
defaultValue={searchParams.get(paramKey) || ''}
>
<option value="">{label}:</option>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.name}
</option>
))}
</select>
</form>
);
};

const STATUS = [
{ value: 'airing', name: 'Airing' },
{ value: 'upcoming', name: 'Upcoming' },
{ value: 'complete', name: 'Complete' },
];

const TYPE = [
{ value: 'tv', name: 'TV' },
{ value: 'movie', name: 'Movie' },
{ value: 'ova', name: 'OVA' },
{ value: 'special', name: 'Special' },
{ value: 'ona', name: 'ONA' },
{ value: 'music', name: 'Music' },
{ value: 'cm', name: 'CM' },
{ value: 'pv', name: 'PV' },
{ value: 'tv_special', name: 'TV Special' },
];

const ORDER_BY = [
{ value: 'mal_id', name: 'Mal id' },
{ value: 'title', name: 'Title' },
{ value: 'start_date', name: 'Start date' },
{ value: 'end_date', name: 'End date' },
{ value: 'episodes', name: 'Episodes' },
{ value: 'score', name: 'Score' },
{ value: 'scored_by', name: 'Scored by' },
{ value: 'rank', name: 'Rank' },
{ value: 'popularity', name: 'Popularity' },
{ value: 'members', name: 'Members' },
{ value: 'favorites', name: 'Favorites' },
];

const SORT_DIRECTION = [
{ value: 'asc', name: 'Ascending' },
{ value: 'desc', name: 'Descending' },
];

export function FilterByStatus() {
return <FilterSelect options={STATUS} paramKey="status" label="Status" />;
}

export function FilterByType() {
return <FilterSelect options={TYPE} paramKey="type" label="Type" />;
}

export function SortByOrder() {
return (
<FilterSelect options={ORDER_BY} paramKey="order_by" label="Order by" />
);
}

export function SortDirection() {
return <FilterSelect options={SORT_DIRECTION} paramKey="sort" label="Sort" />;
}






Modify home page and import filter and sort components from FilterSelect






Explanation



usePathname, useSearchParams, and useRouterHooks

These hooks from next/navigation are essential for managing URL states:





  • usePathname: Retrieves the current path of the URL.


  • useSearchParams: Accesses and manipulates query parameters.


  • useRouter: Provides routing functions to programmatically navigate or update URLs (replace or push).



debouncedCallback in Search Component

The useDebouncedCallback function delays the execution of the search request:




  • Prevents excessive API calls by waiting for a specified duration (500ms).

  • Ensures better performance and smoother user experience.



Dynamic URL Updates

Dynamic updates to URL parameters ensure a smooth, client-side navigation:





  • replace() updates the URL without reloading the page.

  • Keeps the browser history clean while reflecting the filter, sort, and pagination changes.



AnimeList Component and Data Fetching

The AnimeList component handles:




  • Asynchronous data fetching from the Jikan API (getAllAnime()).



Reusable FilterSelect Component

The FilterSelectcomponent:




  • Abstracts dropdown functionality for filters like status, type, order_by, and sort.

  • Simplifies code by reducing redundancy across filterand sortcomponents.



Pagination Handling

The Pagination component:




  • Dynamically generates navigation links (Prev and Next) based on currentPage and totalPages.

  • Disables buttons when reaching the first or last page.



Limit Input Validation

The Limit component validates user input for limiting the number of results:




  • Ensures the value is between 1 and 25.

  • Updates the URL dynamically or resets if the input is invalid.



Suspense for Data Fetching

The Suspense component is used to handle asynchronous rendering:




  • Displays a fallback loading indicator while waiting for data.

  • Ensures the UI remains responsive during data fetches.



and this is how the app looks like:



app looks like 1



app looks like 2



Conclusion

I hope this article has provided a comprehensive guide on how to implement essential features like search, pagination, filtering, sorting, and result limiting in a Next.js application.



Feel free to explore, clone, or contribute to the project on github. I look forward to your feedback and hope this project helps you in your development journey!



References:

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination

https://nextjs.org/docs/app/api-reference/functions/use-search-params

https://docs.api.jikan.moe/

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Next.js API Management: Implement Search, Pagination, Filter, Sort, and Limit Features

Thematisch verwandte Begriffe: Nextjs, Management, Implement, Search · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-59815 | Joplin is an open source note-taking and to-do application that organise…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick