Fetching Data
Overview
Next.js gives you multiple, complementary strategies to fetch data. In this activity, you will focus on building API endpoints with Route Handlers, reading query parameters and dynamic route parameters correctly, and returning clear HTTP responses. You will also compare ORM-based querying with a safe parameterized SQL example.
Prepare
Using the API Layer
API endpoints can be built directly inside your Next.js project using Route Handlers. Any file named route.ts inside app/api/ becomes a server-side endpoint, so you do not need a separate backend for basic API functionality.
app/
└── api/
├── users/
│ └── route.ts → /api/users
├── products/
│ └── route.ts → /api/products
└── products/[id]/
└── route.ts → /api/products/123
Route Handlers create custom request handlers for your API routes using standard web request and response objects. This allows you to define behavior for HTTP methods such as GET, POST, PUT, and DELETE.
Example GET handler for a collection endpoint using an optional query parameter:
import { NextRequest, NextResponse } from 'next/server';
// GET /api/products
// GET /api/products?category=electronics
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
// query your data source and filter by category if provided
return NextResponse.json({ category });
}
The handler reads an optional category query parameter from the URL. When you connect a real data source, you will pass this value to your query function and return the filtered results. When using an asynchronous data source — such as a real database — add a try/catch block around the query to handle connection errors gracefully.
const products = await db.$queryRaw`SELECT * FROM products WHERE category = ${category}`;
Safe — Prisma typed query builder:
const products = await prisma.product.findMany({
where: { category: category }
});
Unsafe — do not do this (string interpolation bypasses parameterization):
const products = await db.$queryRaw(`SELECT * FROM products WHERE category = '${category}'`);
Dynamic Route Segments
Dynamic route segments can be used in API routes to capture parameters from the URL. For example, you can create a dynamic route for fetching a specific product by its ID using square brackets in the file name, such as app/api/products/[id]/route.ts. This allows you to capture the id parameter from the URL and use it to query the database for the corresponding product.
Here is an example of a GET request handler for a dynamic route:
import { NextResponse } from 'next/server';
// GET /api/products/123
export async function GET(
_request: Request,
{ params }: { params: { id: string } }
) {
const id = Number(params.id);
if (Number.isNaN(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
// look up the item by id in your data source
// return NextResponse.json({ error: 'Not found' }, { status: 404 }) if missing
return NextResponse.json({ id });
}
Activity Instructions
Use your portfolio app and the projects routes you created in the layouts activity.
- Confirm your project routes from the layouts activity still work:
- /projects
- /projects/opensource
- /projects/school
- Create lib/projects-db.ts as a simple in-memory data module. This stands in for a real database and is easy to replace later.
Example for lib/projects-db.ts
// lib/projects-db.ts export interface Project { id: number; title: string; description: string; type: 'opensource' | 'school'; technologies: string[]; link?: string; } export const projects: Project[] = [ { id: 1, title: 'My First Open Source Contribution', description: 'A bug fix contributed to a popular library.', type: 'opensource', technologies: ['TypeScript', 'React'], link: 'https://github.com/example/repo' }, { id: 2, title: 'Database Design Final Project', description: 'An ER diagram and normalized schema for a library system.', type: 'school', technologies: ['PostgreSQL', 'SQL'] } ]; export function getProjects(type?: string | null): Project[] { if (type) return projects.filter(p => p.type === type); return projects; } export function getProjectById(id: number): Project | null { return projects.find(p => p.id === id) ?? null; } - Create app/api/projects/route.ts and implement a GET handler that returns all projects. Support optional filtering with ?type=opensource or ?type=school.
Am I allowed to use AI for this activity?
Yes. You are encouraged to use AI as a tutor and coding assistant. You may ask for explanations, debugging help, and small code suggestions. You must understand, test, and be able to explain every line you submit. Avoid generating complete files or large blocks of code that you cannot explain.Review the course AI Policy
- Create app/api/projects/[id]/route.ts and implement a GET handler that reads params.id, validates it, and returns either one project, 400 for invalid id, or 404 if not found.
- Update your project pages to fetch from your new API routes:
- app/projects/page.tsx fetches /api/projects
- app/projects/opensource/page.tsx fetches /api/projects?type=opensource
- app/projects/school/page.tsx fetches /api/projects?type=school
- Test endpoints in the browser or an API client:
- /api/projects
- /api/projects?type=opensource
- /api/projects/1
- /api/projects/abc (should return 400)
- AI Discussion: Why is type a good query parameter (for example, ?type=opensource), while id is a good path parameter (for example, /api/projects/1)?