API Routes Overview
The complete API route tree and shared helpers (getUser, responses)
API Routes Overview
The backend of Odys is structured around a set of API routes, primarily built using Next.js API features. These routes serve as the interface for both the frontend application and external services, handling everything from user authentication and professional profile management to appointment scheduling and payment processing. A core component supporting these routes is src/lib/api.ts, a utility file that centralizes common functionalities like standardized response formats, user authentication helpers, and critical security utilities.
Overview
Odys exposes a total of 23 distinct API routes, each designed to handle specific operations within the application. These routes interact with a database schema comprising 10 tables, including central entities like professionals, clients, and appointments. The API design emphasizes clear separation of concerns, with routes often grouped by the domain they serve.
For instance, core user and professional management is handled by routes such as /api/account, /api/client-profile, /api/onboarding, and /api/settings. Scheduling and booking operations are managed through /api/booking, /api/recurring, and /api/appointments/[id]. Communication features leverage /api/messages and /api/notifications, while integrations with external services like Stripe are found under /api/stripe/checkout, /api/stripe/portal, and /api/stripe/webhook.
A few routes are dedicated to background tasks, specifically /api/cron/reminders and /api/cron/whatsapp-watchdog, which are invoked on a predefined schedule. The /api/cron/reminders route runs daily at 0 8 * * * (8 AM UTC), and /api/cron/whatsapp-watchdog runs daily at 0 9 * * * (9 AM UTC). To protect against abuse and ensure system stability, several endpoints are subject to rate limiting. For example, the /api/booking route is limited to 5 requests per 10 m, while the /api/ai/chat endpoint allows 20 requests per 1 h.
API Route Structure and Helpers
The API routes are organized logically, reflecting the application's features.
Core Application Routes
Many routes directly manage user and professional data. For example, /api/auth/register handles new user sign-ups, while /api/onboarding facilitates the initial setup for professionals. The /api/settings route allows authenticated professionals to update their profile details, which are stored in the professionals table. This table is quite extensive, with 24 columns capturing details from name and slug to sessionDuration, sessionPrice, and various payment-related fields like pixKey and stripeCustomerId.
Client-specific interactions are managed through routes like /api/clients/notes, which allows professionals to add notes about their clients, stored in the clientNotes table. The clients table itself has 8 columns, linking clients to professionals via professionalId and optionally to a Supabase userId.
Scheduling and Communication
The /api/booking route is central to creating and retrieving appointments, which are stored in the appointments table. This table tracks 13 columns, including startsAt, endsAt, status, and paymentStatus. Recurring appointments are managed via /api/recurring, interacting with the recurringSchedules table, which defines 9 columns for frequency and timing.
Communication features include /api/messages for sending and retrieving messages between professionals and clients, stored in the messages table (9 columns). Notifications are handled by /api/notifications, allowing users to fetch and mark notifications as read, which are stored in the notifications table (8 columns).
Integrations and Utilities
Odys integrates with Stripe for payments, with dedicated routes like /api/stripe/checkout for initiating payment flows, /api/stripe/portal for managing subscriptions, and /api/stripe/webhook for processing events from Stripe, such as checkout.session.completed and customer.subscription.updated.
The /api/ai/chat route provides an interface to AI capabilities, likely leveraging tools like get_appointments_today or update_appointment_status from the 4 available MCP tools. File uploads, such as avatars, are managed by /api/upload/avatar.
Shared API Helpers (src/lib/api.ts)
The src/lib/api.ts file provides a set of shared utilities that are imported and used across various API routes, promoting consistency and reducing code duplication.
Response Helpers
A suite of functions standardizes API responses:
ok(data: object): Returns a200 OKresponse with the provided JSON data.unauthorized(): Sends a401 Unauthorizedresponse.forbidden(msg = "Sem permissão"): Returns a403 Forbiddenresponse, allowing for custom messages.notFound(msg = "Não encontrado"): Provides a404 Not Foundresponse.badRequest(msg: string): For400 Bad Requesterrors, requiring a specific message.conflict(msg: string): Returns a409 Conflictresponse, useful for resource conflicts.tooManyRequests(): Sends a429 Too Many Requestsresponse, indicating rate limit exhaustion.serverError(context: string, err: unknown): This critical helper logs the error server-side usinglogger.errorand returns a generic500 Internal Server Errorto the client, preventing sensitive internal details from being exposed.
Authentication Helpers
These functions simplify user and professional identification:
getUser(): Asynchronously fetches the currently authenticated Supabase user. It usescreateClient()from@/lib/supabase/serverto interact with Supabase's authentication service. If no user is logged in, it returnsnull.getProfessional(userId: string): Given a SupabaseuserId, this function queries theprofessionalstable using Drizzle ORM (db,professionals,eq) to retrieve the corresponding professional's profile, ornullif no professional is found. This is a common pattern for routes that require professional-specific data.
Utility Helpers
getIp(req: NextRequest): Extracts the client's IP address from thex-forwarded-forheader, taking the first IP in a comma-separated list and trimming it. It returns"anonymous"if the header is missing or empty. This IP is primarily used for rate limiting.safeEqual(a: string, b: string): This function attempts a constant-time string comparison using Node.js'stimingSafeEqualfor strings of equal length. It returnsfalseearly if inputs are empty or have different lengths, which is not constant-time in those specific cases. This is a security measure to prevent timing side-channel attacks when comparing secrets like API keys or cron tokens, provided the attacker cannot control the length of one of the inputs.isCronAuthorized(req: NextRequest): Verifies if an incoming request is authorized to trigger a cron job. It checks for theCRON_SECRETenvironment variable against either thex-cron-secretheader or anAuthorization: Bearerheader, employingsafeEqualfor secure comparison. This protects the/api/cron/remindersand/api/cron/whatsapp-watchdogendpoints.
Design Decisions
The architectural choices for Odys's API routes and shared helpers reflect a pragmatic approach to building a full-stack application with Next.js.
The decision to centralize API response helpers in src/lib/api.ts was made to enforce consistency in how the API communicates success and various error conditions. By providing specific functions like unauthorized or badRequest, developers are guided towards returning appropriate HTTP status codes and standardized JSON error payloads, which simplifies client-side error handling. The serverError helper is particularly important, as it ensures that internal server errors are logged for debugging while presenting a generic, non-revealing message to the client, a fundamental security practice.
Leveraging Supabase for authentication, as seen in getUser(), offloads the complexities of user management and session handling to a dedicated service. This allows developers to focus on application-specific logic rather than reinventing authentication mechanisms. The subsequent getProfessional() function demonstrates a common pattern of linking a generic user identity to an application-specific profile, in this case, a professional's details from the professionals table.
The inclusion of safeEqual and isCronAuthorized highlights a conscious decision to prioritize security, especially for sensitive operations like cron job invocations. Using a constant-time comparison for secrets mitigates the risk of timing attacks, where an attacker could infer parts of a secret by measuring the time it takes for a comparison function to return. This is a critical detail for protecting backend processes.
Finally, the use of Next.js API routes itself is a design choice that allows for a unified development experience, keeping frontend and backend code within the same repository. This simplifies deployment and allows for easy sharing of types and utilities, such as the Drizzle ORM schema definitions used by getProfessional().
Potential Improvements
-
Enhanced Type Safety for Response Payloads: The
okhelper currently acceptsdata: object. While functional, this could be improved by making it generic, for example,ok<T>(data: T), to provide stronger type guarantees for API consumers. This would allow the TypeScript compiler to verify that the data returned by an API route matches the expected type, reducing runtime errors and improving developer experience.- Reference:
src/lib/api.ts,okfunction.
- Reference:
-
Centralized Rate Limiting Middleware: The
rateLimitsare defined in the system's configuration, andgetIpexists, but the actual enforcement logic for these limits is not present insrc/lib/api.ts. Implementing a reusable middleware function withinsrc/lib/api.tsthat can be applied to specific API routes would centralize rate limit application. This would prevent individual routes from needing to implement their own rate-limiting logic, ensuring consistency and easier maintenance.- Reference:
src/lib/api.ts,getIpfunction.
- Reference:
-
Combined Professional Authentication Helper: The common pattern of first calling
getUser()and thengetProfessional(userId)could be encapsulated into a single helper, perhaps namedgetAuthenticatedProfessional(). This helper would handle both steps, returning theprofessionalobject directly ornullif the user is not authenticated or not registered as a professional. This would streamline the boilerplate in many professional-facing API routes.- Reference:
src/lib/api.ts,getUserandgetProfessionalfunctions.
- Reference:
References
src/lib/api.tssrc/lib/db/schema.tssrc/lib/supabase/server.ts