Hoppa till huvudinnehåll
← Tillbaka till bloggen

From REST API to Server Actions: A Refactoring Journey

From REST API to Server Actions: A Refactoring Journey

The Setup

My portfolio started with traditional REST API routes:

  • POST /api/auth/setup
  • POST /api/posts
  • GET /api/projects
  • POST /api/send-cv
  • ...and 8 more

They worked fine, but there was friction:

  • Manual error handling in every route
  • Verbose fetch() calls on the client
  • No type safety between client and server
  • Duplicated auth checks

Enter Server Actions

Next.js 15+ introduced Server Actions - functions marked with 'use server' that run on the server but can be called directly from client components.

Before (API Route)

// src/app/api/posts/route.ts
export async function POST(req: Request) {
  try {
    const session = await getSession();
    if (!session.isAuthenticated) {
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }
    
    const body = await req.json();
    const post = await db.insertInto('posts').values(body).execute();
    return Response.json(post);
  } catch (error) {
    return Response.json({ error: 'Server error' }, { status: 500 });
  }
}
// Client component
const response = await fetch('/api/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(formData),
});
const data = await response.json();

After (Server Action)

// src/lib/actions/posts.ts
'use server';

export async function createPost(formData: PostFormData) {
  const session = await getSession();
  if (!session.isAuthenticated) {
    redirect('/auth/signin');
  }
  
  const post = await db.insertInto('posts').values(formData).execute();
  return post;
}
// Client component
import { createPost } from '@/lib/actions/posts';

const post = await createPost(formData);

Key Benefits

1. Type Safety End-to-End

No more manual type assertions. TypeScript knows exactly what createPost accepts and returns.

2. No Manual Serialization

Forget JSON.stringify() and response.json(). Arguments and return values are automatically serialized.

3. Error Handling Bubbles Up

Throw errors in server actions, catch them with try/catch on the client. No status codes, no manual error objects.

4. Direct Database Access

Server actions run on the server, so you can import db directly. No need for API layers.

5. Revalidation Built-In

Call revalidatePath('/blog') inside a server action to update cached pages.

When NOT to Use Server Actions

Keep API routes for:

  • Webhooks (external services need HTTP endpoints)
  • Public APIs (rate-limited, token-based access)
  • Edge Runtime (Server actions run in Node.js, not edge)
  • File uploads (FormData works, but API routes give you more control)

Migration Stats

  • 9 API routes deleted
  • 500+ lines of boilerplate removed
  • 0 breaking changes for end users
  • 100% type coverage maintained

Code Organization

I structured actions by domain:

src/lib/actions/
├── auth.ts      # setupAuth, verifyAuthCode, logoutUser
├── posts.ts     # createPost
├── projects.ts  # getProjects, saveProjects, getRepos
├── journey.ts   # CRUD for timeline entries
├── bio.ts       # getBio, updateBio
└── email.ts     # sendCV

Each file exports focused, single-purpose functions.

Lessons Learned

Start with Server Actions if building new features. Only create API routes when you need HTTP access from outside Next.js.

Use redirect() instead of returning errors for auth failures. It's more user-friendly.

Lazy initialization for expensive dependencies (like Resend) prevents module-level errors when env vars are missing.

Type everything - Server actions shine when paired with strict TypeScript.

Performance Impact

Before: Client → API Route → Database (2 network calls) After: Client → Server Action → Database (1 call, co-located)

Server actions are faster because they skip the HTTP overhead between Next.js and itself.

Conclusion

Server Actions aren't just syntactic sugar - they're a fundamental shift in how we build full-stack apps. Less boilerplate, better types, faster execution.

If you're still writing fetch('/api/...'), give Server Actions a try. Your future self will thank you.


Refactored: 9 routes → 9 server actions Code removed: 500+ lines Type errors introduced: 0 Time saved per feature: ~15 minutes