Skip to content

Documentation

This documentation covers the stack’s features, and workflows to help you build and run your ent-stack based project efficiently.

Express

Serves as the backend framework, managing routing, middleware, authentication, and database operations. Its flexibility supports seamless service integration and custom logic.

Next.js

Powers the frontend, offering server-side rendering, static generation, and a robust routing system. It simplifies building SEO-friendly, dynamic UIs with React’s reusable component model.

TRPC

Bridges frontend and backend with type-safe, end-to-end communication, eliminating schema duplication and reducing errors.

Authentication & Authorization

For authentication, a minimal passwordless flow is used:

  1. User inputs email.
  2. Frontend sends the email to the backend.
  3. Backend creates and stores verification PIN to the database and sends to the user’s inbox.
  4. User inputs PIN to the verification form.
  5. The backend validates the PIN against the database. On success, it generates an access token and a refresh token. The refresh token is stored in the database, and both tokens are set as cookies.

Subsequent requests include these tokens in cookies, allowing the backend to revalidate sessions and refresh tokens if needed.

Access Tokens

Access tokens are JWTs used to authenticate users with each request. These tokens are generated during login or registration, stored as HTTP-only cookies, and sent to the client to enable secure interactions with the backend.

  • Stored in cookies to enhance security and prevent client-side manipulation.
  • Used for authenticating API requests.

Refresh Tokens

Refresh tokens are UUIDs that allow users to renew their access tokens when they expire. The backend validates these tokens against the database before issuing a new access token, which is securely set as a server-only cookie in frontend middleware.ts.

  • Stored in the database for validation.
  • Protects against access token expiration without requiring a full re-login.
  • Frontend middleware.ts ensures secure handling of new access tokens.

Verification Tokens

Verification tokens are short PINs designed to confirm email ownership during login or registration. They are used instead of links because clicking a link in a mobile email client often opens a custom in-app browser, different from the user’s main browser, causing potential disruptions.

  • Generated by the backend and stored in the database.
  • Sent to the user’s email as a PIN.
  • Users manually enter the PIN into a verification form on the website.
  • Avoids complications with mobile email client behavior.

Handling invalid attempts

  • Backend sends invalid login attempt when the user tries to login, even though he/she doesn’t have an account.
  • Backend sends invalid registration attempt when the user tries to register, even though he/she already has an account.

Frontend Route Access Protection and Evaluation

  • Route access can be set in routes.ts by setting route property protected to true
  • Access evaluation is done in middleware.ts - it checks if user is authenticated or not

Backend TRPC Route Access Protection and Evaluation

  • Route access can be set by using public or protected Query or Mutation procedure method from trpc.ts
  • Access evaluation is done in authenticationMiddleware in trpc.ts it checks if user is authenticated or not

Configuration

Feature configuration is in <section>-config.ts classes and .env files.

  • These contain secrets like DB credentials, API keys, and environment-specific configuration.
  • The T3 env packages are used for environment variable validation and type safety, both for backend and frontend.

TS configuration

TS configuration is in tsconfig.json files.

  • Main tsconfig.json file is in the root of the monorepo and it is extended by files in apps/backend, apps/frontend, and packages/shared.

Static code analysis configuration

Static code analysis configuration eslint.config.js is the same for backend, frontend and shared code.

Database and ORM

The backend uses Drizzle ORM for interacting with a MySQL database, providing a type-safe and developer-friendly API.

The stack contains Docker image and commands for running the local database.

Terminal window
bin/docker/run-local-db.sh

Configuration

Database connection settings are defined in the .env file and utilized in drizzle.config.ts.

Local Development

Use the command below to quickly apply schema changes during development.

Terminal window
pnpm db:push

Deployment Environments

For controlled updates in test, UAT, or production:

Terminal window
pnpm db:generate // Creates migration files for schema changes
pnpm db:migrate // Applies migrations to update the database schema

Management UI

Run pnpm db:studio to access Drizzle Studio for a graphical interface to view and manage the database.

Mailing

The backend integrates Resend for delivering transactional emails.

Development and Testing

  • Mailslurp is used for testing email functionality in development.
  • A dedicated backend development-only email preview route /email/:wildcard enables rendering email templates directly in the browser for rapid testing.

Email Templates

Templates are built using Handlebars, allowing for dynamic content generation.

Implementation

Email functionality is managed through the following services:

  • email-service.ts: Handles the core logic for sending emails.
  • email-template-service.ts: Manages the creation and rendering of dynamic email templates.

Error Handling

Error handling in this stack is designed to be robust, clean, and developer-friendly, leveraging modern tools and practices for both frontend and backend.

Frontend Error Handling

  • User Feedback: Errors are handled with Sonner for toast notifications and inline field highlighting for invalid user inputs.
  • Validation: Form inputs and user data are validated using Zod, ensuring clear, consistent validation rules across the app.
  • Global Pages: Errors like 404 and server issues are managed with Next.js global-error and not-found pages, ensuring a seamless user experience.

Backend Error Handling

  • Custom Error Classes: The backend uses custom error classes for granular error management and tRPC’s errorFormatter to standardize API responses.
  • Development-Friendly Debugging: TRPC errorFormatter includes a devOnly object during development for detailed error insights.
  • Translated Error Messages: Logical validation errors and server issues are returned as localized, user-friendly messages, minimizing friction for end users.
  • Uncaught Error Management: Express middleware serves as a final safety net, logging and gracefully handling uncaught errors.

Shared Error Handling Utilities

SharedErrorService and ErrorService are used as a practical abstraction around try/catch. Methods of these classes replace repetitive blocks with a tuple-based approach:

  • Success: Returns [null, result]
  • Failure: Returns [error, null]

This makes error handling explicit, highlights error-prone sections, and keeps code clean by separating business logic from error management. It’s a lightweight, JavaScript-native approach that aligns with modern development practices, reducing noise and improving maintainability.

Internationalization (i18n) and Navigation

I18n is treated as a first-class citizen in this stack, providing comprehensive support for message translation, route translation, and navigation helpers.

Message Translation

Messages are handled by standalone functions in the shared package.

These functions utilize the int-messageformat library to format messages and support the ICU Message Format.

Key features:

  • They accept an optional locale parameter, defaulting to the application’s default locale if not provided.
  • These functions are accessible across the backend, frontend applications, and the shared package.

Route Translations

Using Next.js middleware, the app detects the locale from the URL and applies the appropriate translations.

  • Translated routes are handled by middleware.ts using mappings from routes.ts.
  • If you don’t need translations or locale prefixes, remove the route translation logic from middleware and adjust your app structure.
  • A locale prefix is required for all routes, except for the default locale.
  • There is not built-in locale detection, it is up to the user to choose.

Helpers in navigation.ts generate localized URLs and are used in nav-link.tsx and language-switcher.tsx components.

Logging

Both the backend and frontend applications use Pino for logging, with log levels (e.g., info, warn, error) configured through environment variables.

  • Backend additionally uses pino-http for logging HTTP requests and responses.

In development, logs can be formatted for readability using pino-pretty. In production, logs are maintained in structured JSON format.

Styles

Tailwind is used for styling.

Here’s a revised version of your text with improved clarity and formatting, presented as Markdown code:

Testing

Playwright is used for testing both the backend and frontend.

Installation

To set up Playwright, run the following commands:

Terminal window
sudo pnpm exec playwright install-deps
pnpm exec playwright install

Test Commands

The test commands are defined in the top-level package.json:

Backend Tests

Terminal window
pnpm --filter backend test
Terminal window
pnpm --filter backend test-with-trace
Terminal window
pnpm --filter backend test-report

Frontend Tests

Terminal window
pnpm --filter frontend test
Terminal window
pnpm --filter frontend test-with-trace
Terminal window
pnpm --filter frontend test-report

Test Directories

Backend Tests: Located in apps/backend/tests. Includes tests for API endpoints, TRPC functionality, and helper functions.

Frontend Tests: Located in apps/frontend/tests. Includes end-to-end (E2E) tests, such as home.spec.ts and menu.spec.ts.

TRPC

TRPC (Type-safe Remote Procedure Call) is a library for building type-safe APIs in TypeScript.

trpc-client.ts is used for server-to-server or SSR calls (no cookies sent by default).

trpc-client-react.ts is used for browser-based usage, integrated with @tanstack/react-query (query-client.ts) for caching, optimistic updates, and robust async client-side state management.