Environment, HTTP & Building Your First API Route
Environment, HTTP & Building Your First API Route
Step 1 — Set Up Your Environment
Install a REST client in VS Code
You need a tool to send HTTP requests and inspect responses without writing client code. You have two great options built into VS Code:
Option A: Thunder Client (recommended — GUI like Postman)
- In VS Code Extensions, search "Thunder Client" and install it.
- Click the thunder bolt icon in the sidebar. Click "New Request."
- Set the URL to
http://localhost:3000/api/helloand click Send.
Option B: REST Client extension
- Install "REST Client" by Huachao Mao.
- Create a file called
requests.http. Type your request directly:
### Get all courses
GET http://localhost:3000/api/courses
Content-Type: application/json
### Create a course
POST http://localhost:3000/api/courses
Content-Type: application/json
{
"title": "Test Course",
"track": "crash",
"xp": 150
}Click "Send Request" above each block to fire it. Responses appear in a split pane.
Create a Next.js project (if you don't have one)
npx create-next-app@latest my-api-project --typescript --app
cd my-api-project
npm run devWhat IS an HTTP Request? — First Principles
Every time a browser loads a page or your JavaScript calls an API, it sends an HTTP request and receives an HTTP response. Understanding this lifecycle makes everything else make sense.
An HTTP request has four parts:
- Method — What operation? GET, POST, PATCH, DELETE
- URL — What resource?
https://api.example.com/courses/cc-postgres-m01 - Headers — Metadata: content type, auth token, accepted formats
- Body — Data payload (POST/PATCH requests only — GET has no body)
An HTTP response has three parts:
- Status code — What happened? 200, 201, 400, 404, 500
- Headers — Metadata: content type, cache policy, CORS
- Body — The returned data (JSON, HTML, empty)
The full lifecycle:
Client Server
| |
|-- GET /api/courses HTTP/1.1 -----> |
| Host: localhost:3000 |
| Authorization: Bearer eyJ... |
| |
| [query DB]|
| [build JSON]
| |
| <-- HTTP/1.1 200 OK -------------- |
| Content-Type: application/json |
| { "data": [...] } |REST URL Design — Resources, Not Verbs
REST organizes URLs around resources (nouns), not actions (verbs). The HTTP method describes the action.
WRONG (verbs in URL):
GET /api/getCourses
POST /api/createCourse
POST /api/deleteCourse?id=123
RIGHT (resources + methods):
GET /api/courses list all courses
POST /api/courses create a course
GET /api/courses/:id get one course
PATCH /api/courses/:id update a course
DELETE /api/courses/:id delete a course
GET /api/users/:id/courses courses for a user (nested resource)HTTP Methods Mapped to CRUD
GET → Read → safe, idempotent, no body
POST → Create → not idempotent (creates new each time)
PUT → Replace → full replace, idempotent
PATCH → Update → partial update (send only changed fields)
DELETE → Remove → idempotent (deleting twice = same result)HTTP Status Codes — What They Mean
2xx Success:
200 OK — successful GET, PATCH, PUT
201 Created — successful POST (new resource was created)
204 No Content — successful DELETE (no body returned)
4xx Client Error:
400 Bad Request — malformed request or missing required fields
401 Unauthorized — not authenticated (no token or invalid token)
403 Forbidden — authenticated but no permission
404 Not Found — that resource does not exist
422 Unprocessable — valid format but fails validation rules
429 Too Many Requests — rate limited
5xx Server Error:
500 Internal Server Error — something went wrong on the server
503 Service Unavailable — server is down or overloadedBuild a Complete Next.js API Route
// src/app/api/courses/route.ts
import { NextRequest, NextResponse } from 'next/server'
// In-memory "database" for this example
const courses = [
{ id: 'cc-postgres-m01', title: 'SQL Fundamentals', track: 'crash', xp: 150 },
{ id: 'cc-restapi-m01', title: 'REST Fundamentals', track: 'crash', xp: 150 },
]
// GET /api/courses — list all courses, optional ?track= filter
export async function GET(req: NextRequest) {
const track = req.nextUrl.searchParams.get('track')
const result = track ? courses.filter(c => c.track === track) : courses
return NextResponse.json({ data: result, total: result.length })
// Returns 200 OK automatically
}
// POST /api/courses — create a new course
export async function POST(req: NextRequest) {
try {
const body = await req.json()
if (!body.title || !body.track) {
return NextResponse.json(
{ error: 'title and track are required' },
{ status: 400 }
)
}
const newCourse = { id: crypto.randomUUID(), ...body }
courses.push(newCourse)
return NextResponse.json({ data: newCourse }, { status: 201 })
// 201 Created — not 200, because a new resource was created
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
}
}// src/app/api/courses/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const course = courses.find(c => c.id === id)
if (!course) {
return NextResponse.json({ error: 'Course not found' }, { status: 404 })
}
return NextResponse.json({ data: course })
}
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const index = courses.findIndex(c => c.id === id)
if (index === -1) {
return NextResponse.json({ error: 'Course not found' }, { status: 404 })
}
courses.splice(index, 1)
return new NextResponse(null, { status: 204 }) // 204 No Content
}Test these routes in Thunder Client or your .http file — use the correct method (GET vs POST) and send JSON in the body for POST requests.