JSTAcademy
0 XP
Dashboard
Crash Courses
API Routes & Server Actions
11 min
Basic+150 XP
Crash Courses · Basic

API Routes & Server Actions

Build backend endpoints and mutate data with Route Handlers and Server Actions.
11 min read+150 XP on completionCert: Next.js Crash Course
Tap any word in the text below to start reading from there.

API Routes & Server Actions

Next.js gives you two ways to run server-side logic:

  1. Route Handlers — traditional REST API endpoints consumed by external clients, mobile apps, or third-party services
  2. Server Actions — server functions called directly from React components, no HTTP overhead, fully type-safe

Route Handlers

// app/api/courses/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { COURSES } from '@/lib/courses'

export async function GET(req: NextRequest) {
  const track = req.nextUrl.searchParams.get('track')

  const courses = track
    ? COURSES.filter(c => c.track === track)
    : COURSES

  return NextResponse.json(courses)
}

export async function POST(req: NextRequest) {
  const body = await req.json()

  // Validate, save to DB...
  const newCourse = await saveCourse(body)

  return NextResponse.json(newCourse, { status: 201 })
}

The file exports named functions matching HTTP methods: GET, POST, PATCH, DELETE. No framework config — the export name is the method.

Dynamic Route Handler

// app/api/courses/[id]/route.ts
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(course)
}

export async function PATCH(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const updates = await req.json()
  const updated = await updateCourse(id, updates)
  return NextResponse.json(updated)
}

Server Actions — Mutations Without API Routes

Server Actions are async functions marked with 'use server'. They run on the server, but client components can call them like regular JavaScript functions — no fetch, no endpoint, no CORS.

// app/actions/progress.ts
'use server'
import { revalidatePath } from 'next/cache'
import { saveCourseProgress } from '@/lib/db'

export async function markCourseComplete(courseId: string, userId: string) {
  // Runs on the server — can access DB, env vars, secrets directly
  await saveCourseProgress({ courseId, userId, completedAt: new Date() })

  // Clear the cache so the next page render shows updated progress
  revalidatePath('/courses')
  revalidatePath(`/courses/${courseId}`)

  return { success: true }
}
// app/components/CompleteButton.tsx
'use client'
import { useState } from 'react'
import { markCourseComplete } from '@/app/actions/progress'

export function CompleteButton({ courseId, userId }: { courseId: string; userId: string }) {
  const [loading, setLoading] = useState(false)
  const [done, setDone] = useState(false)

  async function handleClick() {
    setLoading(true)
    await markCourseComplete(courseId, userId)  // calls server directly
    setDone(true)
    setLoading(false)
  }

  return (
    <button onClick={handleClick} disabled={loading || done}>
      {done ? 'Completed!' : loading ? 'Saving...' : 'Mark Complete'}
    </button>
  )
}

No API route. No endpoint URL. No fetch. The client button calls a server function by reference.

When to Use What

Use CaseTool
Form submit, toggle, mutation from a React componentServer Action
REST API for a mobile app or external serviceRoute Handler
Webhook endpointRoute Handler
Database mutation from a button clickServer Action
GraphQL endpointRoute Handler

Error Handling in Route Handlers

export async function POST(req: NextRequest) {
  try {
    const body = await req.json()

    if (!body.title) {
      return NextResponse.json({ error: 'title is required' }, { status: 400 })
    }

    const result = await saveItem(body)
    return NextResponse.json(result, { status: 201 })
  } catch (error) {
    console.error('POST /api/items error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}
0%