Nuxt Server Routes — Building API Endpoints in Nuxt 3
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
- Not using
defineEventHandler: Every server route must export a handler wrapped indefineEventHandler. - Throwing plain errors: Always use
createErrorwithstatusCodeandstatusMessagefor proper error responses. - Not validating input: Validate
readBody,getQuery, andgetRouterParambefore using them. - No error handling for database calls: Wrap DB operations in try-catch and return appropriate error responses.
- Exposing sensitive data: Don't return passwords or tokens from API routes. Filter response data.
Practice Questions
Where do server API routes live? Answer: In
server/api/. Each.tsfile becomes an API endpoint at/api/filename.How do you read the request body in a server route? Answer: Use
await readBody(event). It parses JSON request bodies automatically.How do you access route parameters? Answer: Use
getRouterParam(event, 'paramName'). The route file must include[paramName]in its filename.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
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