Skip to content

Nuxt Server Routes — Building API Endpoints in Nuxt 3

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Nuxt Server Routes. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Nuxt 3 server routes: creating API endpoints, handling requests and responses, and building a full-stack Nuxt application without a separate backend.

In this lesson, you'll understand how to create server routes in the server/ directory, handle HTTP methods, and connect them to your frontend.

What You'll Learn

How to create server API routes, handle GET/POST/PUT/DELETE requests, use server utilities, and connect server routes to your pages.

Why It Matters

Server routes eliminate the need for a separate backend server for many applications. Nuxt's Nitro engine handles routing, caching, and deployment.

flowchart LR
    A[server/api/] --> B[posts.ts /api/posts]
    A --> C[posts/[id].ts /api/posts/:id]
    B --> D[Database]
    C --> D
    E[Frontend] -->|useFetch| B
    E -->|useFetch| C
    style B fill:#00dc82,color:#fff
    style C fill:#00855a,color:#fff

Basic API Route

Create server/api/hello.ts:

export default defineEventHandler((event) => {
  return {
    message: 'Hello from Nuxt server!',
    timestamp: new Date().toISOString()
  };
});

Output: GET /api/hello returns { message: "Hello...", timestamp: "..." }. Access it with useFetch('/api/hello') in pages.

Route Parameters

// server/api/posts/[id].ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');
  
  // Validate
  if (!id || isNaN(Number(id))) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid ID'
    });
  }

  const post = await db.posts.findUnique({
    where: { id: Number(id) }
  });

  if (!post) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Post not found'
    });
  }

  return post;
});

Handling HTTP Methods

// server/api/posts/index.ts
export default defineEventHandler(async (event) => {
  const method = getMethod(event);

  switch (method) {
    case 'GET': {
      const posts = await db.posts.findMany({
        orderBy: { createdAt: 'desc' },
        take: 10
      });
      return posts;
    }

    case 'POST': {
      const body = await readBody(event);
      
      if (!body.title || !body.content) {
        throw createError({
          statusCode: 400,
          statusMessage: 'Missing required fields'
        });
      }

      const post = await db.posts.create({
        data: {
          title: body.title,
          content: body.content,
          authorId: event.context.auth?.userId
        }
      });

      return { success: true, id: post.id };
    }

    default:
      throw createError({
        statusCode: 405,
        statusMessage: 'Method not allowed'
      });
  }
});

Middleware in Server Routes

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  const token = getHeader(event, 'authorization');

  if (token) {
    try {
      const user = await verifyToken(token.replace('Bearer ', ''));
      event.context.auth = { userId: user.id, role: user.role };
    } catch {
      // Token invalid — proceed without auth
    }
  }
});
// server/api/admin/users.ts
export default defineEventHandler(async (event) => {
  // Access auth set by middleware
  if (event.context.auth?.role !== 'admin') {
    throw createError({
      statusCode: 403,
      statusMessage: 'Admin access required'
    });
  }

  return await db.users.findMany();
});

Query Parameters

// server/api/posts/index.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const page = Number(query.page) || 1;
  const limit = Number(query.limit) || 10;
  const search = query.search as string || '';

  const where = search
    ? { title: { contains: search, mode: 'insensitive' } }
    : {};

  const [posts, total] = await Promise.all([
    db.posts.findMany({ where, skip: (page - 1) * limit, take: limit }),
    db.posts.count({ where })
  ]);

  return {
    posts,
    pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }
  };
});

Connecting to Frontend

<script setup>
const page = ref(1);
const search = ref('');

const { data, refresh } = await useFetch('/api/posts', {
  params: { page, limit: 10, search },
  watch: [page, search]  // Refetch when these change
});

const createPost = async () => {
  await $fetch('/api/posts', {
    method: 'POST',
    body: { title: 'New Post', content: '...' }
  });
  refresh();  // Reload the list
};
</script>

Common Mistakes

  1. Not using defineEventHandler: Every server route must export a handler wrapped in defineEventHandler.
  2. Throwing plain errors: Always use createError with statusCode and statusMessage for proper error responses.
  3. Not validating input: Validate readBody, getQuery, and getRouterParam before using them.
  4. No error handling for database calls: Wrap DB operations in try-catch and return appropriate error responses.
  5. Exposing sensitive data: Don't return passwords or tokens from API routes. Filter response data.

Practice Questions

  1. Where do server API routes live? Answer: In server/api/. Each .ts file becomes an API endpoint at /api/filename.

  2. How do you read the request body in a server route? Answer: Use await readBody(event). It parses JSON request bodies automatically.

  3. How do you access route parameters? Answer: Use getRouterParam(event, 'paramName'). The route file must include [paramName] in its filename.

  4. How do you throw a proper HTTP error? Answer: Use throw createError({ statusCode: 404, statusMessage: 'Not found' }).

Challenge

Build a complete REST API for a todo app with: GET /api/todos, POST /api/todos, PUT /api/todos/:id, DELETE /api/todos/:id. Add validation and error handling.

Mini Project

Create a recipe sharing app with server routes: GET /api/recipes (with search and pagination), POST /api/recipes (create), GET /api/recipes/:id (detail), POST /api/recipes/:id/rate (rating), and GET /api/categories.

FAQ

Can I use a database in server routes?

: Yes. Nuxt server routes support any database. Use Prisma, Drizzle, SQLite, PostgreSQL, or MongoDB.

Are server routes auto-imported?

: Yes. Nitro auto-imports defineEventHandler, getQuery, readBody, createError, and other server utilities.

Do server routes work during static generation?

: No. Server routes require a server runtime. Use static generation for fully static sites without API routes.

Can I deploy server routes separately?

: Yes. Nuxt 3 can deploy server routes as serverless functions on Vercel, Netlify, Cloudflare, or Node.js.

What's Next

Learn about Nuxt Middleware to protect routes, redirect users, and run logic before page rendering.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro