Skip to content

Backend API Endpoints

To ensure consistency and simplicity in building backend API endpoints, we use Next.js Route Handlers as the foundation, combined with a shared core wrapper that handles common logic such as role/permission-based access control and data validation.

REST API Conventions

Although following strict RESTful URL conventions in the Next.js framework can be tedious, our team has agreed on a standardized approach to keep things as RESTful as possible. Each endpoint is structured to support typical CRUD operations along with common extensions:

  • CREATE: Create a new resource
  • READ: Retrieve a single resource
  • UPDATE: Update an existing resource
  • DELETE: Delete a resource
  • LIST: Retrieve a list of resources
  • SEARCH: Search resources with more flexible criteria beyond pagination

Directory Structure

Our RESTful API endpoints are organized by database entities. For each entity:

  • Use a pluralized directory name (e.g., products)
  • Add a route.ts file for collection-level operations (CREATE, LIST, SEARCH)
  • Add a [id]/route.ts file for resource-level operations (READ, UPDATE, DELETE)

API Handler Wrapper

A shared core module (createApiHandler) encapsulates common backend logic, including:

  • Role/permission-based privilege checks
  • Request validation (path params, query params, and body)

This helps reduce boilerplate and ensure consistent enforcement of access control and data validation.

Examples

Collection-Level Operations (/app/api/products/route.ts)

import { z } from 'zod';
import { GlobalRole } from '@/constants';
import createApiHandler from '@/core/api-handler';
import createOp from './_operations/create';
import listOp from './_operations/list';

// CREATE
export const POST = createApiHandler({
    roles: [GlobalRole.Admin],
    validations: {
        body: z.object({
            name: z.string(),
            age: z.number(),
        }),
    },
})(async ({ session, body }) => {
    return await createOp({ session, body });
});

// LIST
export const GET = createApiHandler({
    roles: [GlobalRole.User],
    validations: {
        queryParams: z.object({
            page: z.number(),
            pageSize: z.number(),
        }),
    },
})(async ({ session }) => {
    return await listOp({ session });
});

Resource-Level Operations (/app/api/products/[id]/route.ts)

import { z } from 'zod';
import { GlobalRole } from '@/constants';
import createApiHandler from '@/core/api-handler';
import readOp from '../_operations/read';
import updateOp from '../_operations/update';
import deleteOp from '../_operations/delete';

// READ
export const GET = createApiHandler({
    roles: [GlobalRole.User],
    validations: {
        pathParams: z.object({
            licencePlate: z.string(),
        }),
    },
})(async ({ session, pathParams }) => {
    return await readOp({ session, pathParams });
});

// UPDATE
export const PUT = createApiHandler({
    roles: [GlobalRole.Admin],
    validations: {
        pathParams: z.object({
            licencePlate: z.string(),
        }),
        body: z.object({
            name: z.string(),
            age: z.number(),
        }),
    },
})(async ({ session, pathParams, body }) => {
    return await updateOp({ session, pathParams, body });
});

// DELETE
export const DELETE = createApiHandler({
    roles: [GlobalRole.Admin],
    validations: {
        pathParams: z.object({
            licencePlate: z.string(),
        }),
    },
})(async ({ session, pathParams }) => {
    return await deleteOp({ session, pathParams });
});