lindis-api-endpoints
Context and patterns for building Nitro server API endpoints in Lindi's Store. Load when creating, modifying, or debugging API routes in server/api/.
How do I install this agent skill?
npx skills add https://github.com/adzamkomladev/lindis-store --skill lindis-api-endpointsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides development patterns and coding standards for building API endpoints in a Nuxt 4 application. It emphasizes security best practices, including input validation with Zod, authentication middleware, and HMAC signature verification for webhooks. No security risks or malicious patterns were identified.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 1 issue
What does this agent skill do?
Server API Endpoints — Lindi's Store
Context skill for building and modifying API routes in this Nuxt 4 + MongoDB + RivetKit project.
Announce: "I'm loading lindis-api-endpoints context for API development."
Key Facts
- Framework: Nuxt 4 with Nitro server (Node.js preset,
node-server) - Database: MongoDB via
collections()helper fromserver/db/collections.ts - Actors: RivetKit actors via
useRivet()fromserver/rivet/client.ts - Auth:
nuxt-auth-utils—requireUserSession(event)for admin, public for storefront - Validation: Zod schemas from
schemas/directory - Auto-imports:
server/utils/*are auto-imported — DO NOT manually import them - WebSocket: Nitro experimental WebSocket enabled for RivetKit actor events
API Endpoint Pattern
// server/api/orders/initiate.post.ts
import { initiateOrderSchema } from '~~/schemas/order.schema'
import { nanoid } from 'nanoid'
import { useRivet } from '~/server/rivet/client'
export default defineEventHandler(async (event) => {
// 1. Validate request body
const body = await readValidatedBody(event, initiateOrderSchema.parse)
// 2. Business logic — use actors or direct MongoDB
const rivet = useRivet()
const orderNumber = `ls-${nanoid(8).toLowerCase()}`
const orderActor = rivet.orderActor.getOrCreate([orderNumber])
// 3. Push command to actor workflow
await orderActor.commands.push({ type: 'initiate', ... })
// 4. Return response
return { orderNumber, reference }
})
Authentication Patterns
Admin-Only Routes
Admin routes are protected by server/middleware/admin.ts which checks all /api/admin/** paths:
// server/middleware/admin.ts automatically runs before admin handlers
// It checks: getUserSession(event).user.role === 'admin'
// No need to manually check in individual handlers
// server/api/admin/products.post.ts
export default defineEventHandler(async (event) => {
// Admin auth already verified by middleware
const session = await getUserSession(event)
// session.user.email, session.user.role available
})
Public Routes (Storefront)
Storefront routes have no auth requirement:
// server/api/products/index.get.ts — public product listing
export default defineEventHandler(async (event) => {
const rivet = useRivet()
const products = await rivet.catalogActor.getOrCreate(['main']).getProducts()
return products
})
Token-Based Routes (Reviews)
Some routes use token-based auth, not user sessions:
// server/api/reviews/submit.post.ts — validates reviewToken, not user session
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, reviewSubmitSchema.parse)
const { reviewTokens } = collections()
const token = await reviewTokens.findOne({ token: body.token })
if (!token || token.usedAt || token.expiresAt < new Date()) {
return sendError(event, createError({ statusCode: 400, statusMessage: 'Invalid or expired token' }))
}
// ... process review
})
Validation Patterns
Server-Side Validation
import { productSchema } from '~~/schemas/product.schema'
import { initiateOrderSchema } from '~~/schemas/order.schema'
// Body validation
const body = await readValidatedBody(event, productSchema.parse)
// Query validation
const query = await getValidatedQuery(event, querySchema.parse)
// Route params
const id = getRouterParam(event, 'id')
// ObjectId validation (in schemas)
const objectIdString = z.string().regex(/^[a-f\d]{24}$/i, 'Invalid ID')
Schema Location Convention
Schemas live in the root schemas/ directory, NOT inline in components or API handlers:
// schemas/product.schema.ts — always define here
// schemas/order.schema.ts
// schemas/review.schema.ts
// etc.
// Import in handlers
import { productSchema } from '~~/schemas/product.schema'
Error Handling
// Client-friendly errors
return sendError(
event,
createError({
statusCode: 400,
statusMessage: 'Product slug is already in use',
}),
)
// Common status codes:
// 400 — Bad Request (validation, duplicate, invalid)
// 401 — Unauthorized
// 403 — Forbidden (no admin permission)
// 404 — Not Found
// 500 — Server Error
// RivetKit business validation errors
import { UserError } from 'rivetkit'
throw new UserError('Insufficient stock', {
code: 'out_of_stock',
metadata: { productId, requested: qty, available: current },
})
Actor Interaction Patterns
Singleton Actors (catalog, inventory, analytics, settings, workers)
const rivet = useRivet()
// Read from in-memory catalog
const products = await rivet.catalogActor.getOrCreate(['main']).getProducts()
// Check stock
const stock = await rivet.inventoryActor.getOrCreate(['main']).getStock(productId)
// Get dashboard stats
const stats = await rivet.analyticsActor.getOrCreate(['main']).getDashboardStats()
Per-Entity Actors (order, cart)
const rivet = useRivet()
// Order actor — keyed by orderNumber
const order = await rivet.orderActor.getOrCreate([orderNumber]).getOrder()
// Cart actor — keyed by session ID (conceptual, current implementation uses client-side cart)
const cart = await rivet.cartActor.getOrCreate([sessionId]).getCart()
Queue-Based Interactions
import { enqueueEmail, enqueuePayment, enqueueReviewRequest } from '~/server/utils/queues'
// These are auto-imported — DO NOT import them manually
// Enqueue email
await enqueueEmail({
to: 'customer@example.com',
subject: 'Order Confirmed',
templateId: 'order_confirmation',
data: { orderNumber: 'ls-abc12345' },
})
// Enqueue payment verification
await enqueuePayment({
reference: 'pay-ls-abc12345',
orderNumber: 'ls-abc12345',
amount: 50000, // pesewas
})
// Enqueue review request
await enqueueReviewRequest({
orderId: '...',
orderNumber: 'ls-abc12345',
email: 'customer@example.com',
customerName: 'John Doe',
items: [...],
})
Direct MongoDB Fallback
For read-only queries that don't need actor state, use MongoDB directly:
import { collections } from '~/server/db/collections'
export default defineEventHandler(async (event) => {
const { products } = collections()
// Simple read — no actor needed
const product = await products.findOne({ slug: params.slug, status: 'active' })
// Aggregation
const stats = await orders.aggregate([
{ $match: { paymentStatus: 'paid' } },
{ $group: { _id: null, totalRevenue: { $sum: '$total' } } },
]).toArray()
return product
})
Important: Use actors for any operation that involves:
- Product catalog reads (in-memory, sub-ms)
- Stock checks or reservations (atomic consistency)
- Order workflow steps (durable via RivetKit)
- Settings reads (in-memory)
Use MongoDB directly for:
- Admin dashboard aggregations (analytics actor caches these)
- One-off queries that don't need actor state
- Search/filter queries with complex criteria
Response Format Convention
// Success responses — return plain objects
return { products, total, page }
// Created resources — return the created document
return product // the full ProductDoc
// Empty success
return { success: true }
// Paginated responses
return {
data: products,
total: count,
page: pageNum,
pageSize: pageSizeNum,
}
File Organization
server/api/
├── admin/ # Admin-only (auth middleware runs automatically)
│ ├── products/ # Product CRUD
│ │ ├── index.get.ts # List products (all statuses)
│ │ ├── index.post.ts # Create product
│ │ └── [id]/
│ │ ├── index.get.ts # Get single product
│ │ ├── index.put.ts # Update product
│ │ └── index.delete.ts # Delete product
│ ├── orders/ # Order management
│ │ ├── index.get.ts # List orders
│ │ ├── recent.get.ts # Recent orders
│ │ └── [id]/
│ │ ├── index.get.ts # Get order
│ │ └── [id].put.ts # Update order
│ ├── users/ # User management
│ ├── discount-codes/ # Coupon CRUD
│ ├── payments/ # Payment tracking
│ ├── settings/ # Settings read/update
│ ├── stats.get.ts # Dashboard stats
│ └── revenue.get.ts # Revenue chart data
├── products/ # Public product endpoints
│ ├── index.get.ts # List active products
│ ├── featured.get.ts # Featured products
│ └── [slug].get.ts # Product by slug
├── orders/ # Order endpoints
│ ├── initiate.post.ts # Create order + Paystack init
│ ├── verify.get.ts # Verify payment (redirect)
│ └── verify-payment.post.ts # Verify payment (API)
├── reviews/ # Review endpoints
│ ├── submit.post.ts # Submit review
│ ├── validate.get.ts # Validate review token
│ └── [productId].get.ts # Get product reviews
├── discount-codes/ # Discount validation
│ └── validate.post.ts # Validate discount code
├── images/ # Image proxy
│ └── upload.post.ts # Upload image to R2
├── stats.get.ts # Public stats
├── settings/
│ └── banner.get.ts # Get banner text
├── auth/ # Authentication
│ ├── login.post.ts # Admin login
│ └── logout.post.ts # Admin logout
├── setup-admin.post.ts # One-time admin setup
└── webhooks/
└── paystack.post.ts # Paystack webhook
Paystack Integration
// Initialize transaction (used in initiate.post.ts)
const { initializeTransaction } = usePaystack()
const result = await initializeTransaction({
email: 'customer@example.com',
amount: 50000, // pesewas
reference: 'pay-ls-abc12345',
callback_url: `${config.public.baseUrl}/api/orders/verify`,
metadata: { orderNumber: 'ls-abc12345' },
})
// result.authorization_url — redirect customer here
// Verify transaction (used in payment-worker.ts)
const { verifyTransaction } = usePaystack()
const data = await verifyTransaction(reference)
Webhook Pattern
// server/api/webhooks/paystack.post.ts
import { createHmacSha512 } from '~/server/utils/hmac'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const signature = getHeader(event, 'x-paystack-signature')
// Verify HMAC signature
const config = useRuntimeConfig()
const expectedSig = await createHmacSha512(JSON.stringify(body), config.paystackSecretKey)
if (signature !== expectedSig) {
return sendError(event, createError({ statusCode: 401, statusMessage: 'Invalid signature' }))
}
// Process webhook event
if (body.event === 'charge.success') {
await enqueuePayment({ reference: body.data.reference, ... })
}
return { received: true }
})
R2 Image Upload
import { useR2 } from '~/server/utils/r2'
// Upload
const { upload, getPublicUrl } = useR2()
await upload(key, buffer, contentType)
const url = getPublicUrl(key)
// Upload route example (server/api/images/upload.post.ts)
export default defineEventHandler(async (event) => {
const formData = await readMultipartFormData(event)
const file = formData?.find(f => f.name === 'file')
if (!file) return sendError(event, createError({ statusCode: 400, statusMessage: 'No file' }))
const key = `products/${Date.now()}-${file.filename}`
const { upload, getPublicUrl } = useR2()
await upload(key, file.data, file.type!)
return { url: getPublicUrl(key) }
})
Do NOT
- Import from
server/utils/*manually — they are auto-imported by Nitro - Use
requireUserSession()on public storefront routes - Forget to validate request bodies with Zod schemas
- Store monetary values as floats — always use pesewas (integers)
- Skip admin auth on admin routes (the middleware handles it, but don't bypass it)
- Create new database collections without adding types to
server/db/types.tsand indexes toserver/db/indexes.ts - Use
~/server/in server imports — use~/server/or the alias fromnuxt.config.ts - Forget to handle the case where RivetKit runner is unavailable (
isRivetRunnerUnavailable())
Reference Files
- Example POST:
server/api/orders/initiate.post.ts - Example admin GET:
server/api/admin/stats.get.ts - Example admin CRUD:
server/api/admin/products.post.ts - Actor client:
server/rivet/client.ts - Queue helpers:
server/utils/queues.ts - Paystack:
server/utils/paystack.ts - Email:
server/utils/email.ts - HMAC:
server/utils/hmac.ts - R2:
server/utils/r2.ts - Middleware:
server/middleware/admin.ts - Collections:
server/db/collections.ts - Schemas:
schemas/*.schema.ts
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/adzamkomladev/lindis-store/lindis-api-endpoints">View lindis-api-endpoints on skillZs</a>