404 Not Found

404 Not Found


nginx

Server-Side API Routes

Bob needs to add backend APIs to MegaShop—including CRUD operations for products, shopping cart functionality, and user authentication. The traditional approach would require deploying a separate Express server, which makes frontend-backend integration a headache. Charlie discovered that Nuxt 3 allows you to write APIs directly in the server/ directory, handling both frontend and backend within a single project.

1. What You'll Learn


2. A True Story of an Administrator

(1) Pain Point: The Nightmare of Integrating Separate Front-End and Back-End Systems

Bob wrote a standalone API server using Express, running on port 4000. The Nuxt frontend runs on port 3000. During development, he had to configure a proxy and handle cross-origin requests, and deployment required two separate CI/CD pipelines. Alice's shopping cart requests were occasionally blocked due to cross-origin issues, resulting in errors in production.

(2) Solution for the Nuxt Server API

In Nuxt 3, the server/ directory allows you to write APIs directly within the project; since they are on the same domain and port, there are no cross-domain issues:

TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(() => {
  return { items: products, total: 1000000 }
})

(3) Benefits: All-in-One Full-Stack Solution

Bob maintains only one project, with the API and front end deployed on the same domain, so Alice will never encounter cross-domain errors again, and the deployment process is simplified by 50%.


3. API Route Definitions

(1) File Routing Mapping Rules

File Path HTTP Method Route URL
server/api/products.ts ALL /api/products
server/api/products/index.ts ALL /api/products
server/api/products/index.get.ts GET /api/products
server/api/products/index.post.ts POST /api/products
server/api/products/[id].get.ts GET /api/products/:id
server/api/products/[id].put.ts PUT /api/products/:id
server/api/products/[id].delete.ts DELETE /api/products/:id
server/api/auth/login.post.ts POST /api/auth/login

(2) API Request Processing Lifecycle

100%
sequenceDiagram
    participant C as Client
    participant M as Server Middleware
    participant H as Event Handler
    participant D as Data Source

    C->>M: HTTP Request
    M->>M: Auth check / CORS / Logging
    M->>H: Pass event
    H->>D: Query / Mutation
    D-->>H: Data result
    H-->>M: HTTP Response
    M-->>C: JSON Response

(1) ▶ Example: GET Product List

TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler((event) => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  const limit = Number(query.limit) || 20
  const category = query.category as string

  let filtered = mockProducts
  if (category) {
    filtered = filtered.filter(p => p.category === category)
  }

  const start = (page - 1) * limit
  return {
    items: filtered.slice(start, start + limit),
    total: filtered.length,
    page,
    limit
  }
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: POST to Create a Product

TYPESCRIPT
// server/api/products/index.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Validate required fields
  if (!body.name || !body.price) {
    throw createError({
      statusCode: 400,
      message: 'Name and price are required'
    })
  }

  const newProduct = {
    id: mockProducts.length + 1,
    name: body.name,
    price: Number(body.price),
    category: body.category || 'uncategorized',
    inStock: body.inStock ?? true,
    image: body.image || '/images/placeholder.webp'
  }

  mockProducts.push(newProduct)
  return { product: newProduct, message: 'Product created successfully' }
})

Output:

TEXT
// Execution Successful

(3) ▶ Example: Dynamic Parameter Routing

TYPESCRIPT
// server/api/products/[id].get.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  const product = mockProducts.find(p => p.id === id)

  if (!product) {
    throw createError({
      statusCode: 404,
      message: 'Product not found'
    })
  }

  return product
})

Output:

TEXT
// Execution Successful

(4) ▶ Example: PUT to Update a Product

TYPESCRIPT
// server/api/products/[id].put.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  const body = await readBody(event)
  const index = mockProducts.findIndex(p => p.id === id)

  if (index === -1) {
    throw createError({ statusCode: 404, message: 'Product not found' })
  }

  mockProducts[index] = { ...mockProducts[index], ...body }
  return { product: mockProducts[index], message: 'Product updated' }
})

Output:

TEXT
// Execution Successful

(5) ▶ Example: DELETE—Delete a product

TYPESCRIPT
// server/api/products/[id].delete.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  const index = mockProducts.findIndex(p => p.id === id)

  if (index === -1) {
    throw createError({ statusCode: 404, message: 'Product not found' })
  }

  const deleted = mockProducts.splice(index, 1)
  return { product: deleted[0], message: 'Product deleted' }
})

Output:

TEXT
// Execution Successful

4. Event-handling utility functions

(1) Quick Reference for Request Tools

Function Purpose Example
getQuery(event) Retrieve URL query parameters { page: '1', limit: '20' }
getRouterParam(event, key) Get route parameter /products/123 → '123'
readBody(event) Read request body { name: 'Product', price: 99 }
getHeader(event, key) Get request header 'Bearer token...'
getCookie(event, key) Get Cookie 'session-id-xxx'
setCookie(event, key, val, opts) Set a cookie setCookie(event, 'token', jwt, { httpOnly: true })
setHeader(event, key, val) Set response header setHeader(event, 'x-total', '1000')
createError(opts) Throw an error createError({ statusCode: 404 })

(2) Response Format Conventions

Scenario Status Code Response Body
Query successful 200 { items: [], total: 0 }
Created successfully 201 { product: {}, message: '...' }
Update successful 200 { product: {}, message: '...' }
Deleted successfully 200 { message: '...' }
Parameter error 400 { statusCode: 400, message: '...' }
Unauthenticated 401 { statusCode: 401, message: '...' }
Not Found 404 { statusCode: 404, message: '...' }

5. Server Middleware

(1) ▶ Example: Global Log Middleware

TYPESCRIPT
// server/middleware/logger.ts
export default defineEventHandler((event) => {
  const start = Date.now()
  const method = getMethod(event)
  const url = getRequestURL(event)

  // Log after response
  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode
    console.log(`${method} ${url} → ${status} (${duration}ms)`)
  })
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: Auth middleware

TYPESCRIPT
// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const protectedPaths = ['/api/admin', '/api/orders', '/api/cart']

  const url = getRequestURL(event)
  const isProtected = protectedPaths.some(path => url.pathname.startsWith(path))

  if (!isProtected) return

  const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
  if (!token) {
    throw createError({ statusCode: 401, message: 'Authentication required' })
  }

  // Verify JWT token (simplified)
  try {
    const payload = verifyToken(token)
    event.context.user = payload
  } catch {
    throw createError({ statusCode: 401, message: 'Invalid token' })
  }
})

Output:

TEXT
// Execution Successful

6. CORS Cross-Origin Handling

(1) ▶ Example: Nitro's Built-in CORS Configuration

TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/api/**': { cors: true }
  }
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: Custom CORS Middleware

TYPESCRIPT
// server/middleware/cors.ts
export default defineEventHandler((event) => {
  const origin = getHeader(event, 'origin') || '*'

  setHeader(event, 'Access-Control-Allow-Origin', origin)
  setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')
  setHeader(event, 'Access-Control-Max-Age', '86400')

  // Handle preflight
  if (getMethod(event) === 'OPTIONS') {
    event.node.res.statusCode = 204
    return ''
  }
})

Output:

TEXT
// Execution Successful

(1) Comparison of CORS Solutions

Solution Configuration Method Flexibility Use Cases
routeRules cors: true nuxt.config.ts Low (All On/All Off) Development Environment
Custom middleware server/middleware/ High (fine-grained) Production
Nitro routeRules Advanced routeRules per-route Medium Mixed Requirements

7. Comprehensive Example: The MegaShop API Framework

TYPESCRIPT
// server/api/cart/index.get.ts - Get cart items
export default defineEventHandler((event) => {
  const userId = event.context.user?.id
  if (!userId) {
    throw createError({ statusCode: 401, message: 'Login required' })
  }

  const cart = mockCarts.find(c => c.userId === userId)
  return cart || { userId, items: [], total: 0 }
})
TYPESCRIPT
// server/api/cart/index.post.ts - Add item to cart
export default defineEventHandler(async (event) => {
  const userId = event.context.user?.id
  const { productId, quantity = 1 } = await readBody(event)

  const product = mockProducts.find(p => p.id === productId)
  if (!product) {
    throw createError({ statusCode: 404, message: 'Product not found' })
  }
  if (!product.inStock) {
    throw createError({ statusCode: 400, message: 'Product out of stock' })
  }

  // Add or update cart item
  let cart = mockCarts.find(c => c.userId === userId)
  if (!cart) {
    cart = { userId, items: [], total: 0 }
    mockCarts.push(cart)
  }

  const existing = cart.items.find(i => i.productId === productId)
  if (existing) {
    existing.quantity += quantity
  } else {
    cart.items.push({ productId, name: product.name, price: product.price, quantity })
  }

  cart.total = cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
  return { cart, message: 'Item added to cart' }
})

❓ FAQ

Q Is the code in the server/ directory included in the client bundle?
A No. Nitro bundles the server/ code separately, so it does not leak to the client. The server code can safely use Node.js APIs and secrets.
Q Are the .get and .post suffixes required in API route file names?
A No, they are not required. Files without method suffixes handle all HTTP methods. Adding suffixes allows for precise control over the handling logic for each method, so it is recommended.
Q What is the difference between createError and throw new Error?
A createError is provided by H3 and returns the correct HTTP status code and a JSON response. throw new Error returns a 500 internal error. We recommend using createError in API routes.
Q What is the difference between server middleware and client middleware?
A Server middleware is located in server/middleware/ and intercepts API requests (authorization, logging, CORS). Client middleware is located in the middleware/ directory and intercepts page route transitions (permission guards/redirects).
Q How do I use the private variables of runtimeConfig in the API?
A Simply use useRuntimeConfig() to retrieve them; private variables are only available on the server: const config = useRuntimeConfig()config.databaseUrl.
Q Can the API return a stream response (SSE)?
A Yes. You can write data in chunks using event.node.res.write() and set Content-Type: text/event-stream to implement SSE. Nitro fully supports the native Node.js response API.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Create the GET /api/products and GET /api/products/[id] endpoints, which return mock product data.
  2. Advanced Exercise (Difficulty: ⭐⭐): Implement a complete CRUD API (GET/POST/PUT/DELETE), including parameter validation and error handling.
  3. Challenge (Difficulty: ⭐⭐⭐): Implement authentication using server-side middleware. Return a 401 error when an unauthenticated user accesses /api/cart; allow normal operations after login.

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏