Skip to main content

API (NestJS)

General

For new requirements, always check the documentation first. There are already established techniques or patterns for many common problems.

NestJS is agnostic in many ways, and one of them is communication. It provides wrappers (or allows us to create our own) for third-party libraries.

See: What is the NestJS Runtime - Trilon Consulting

Logging

  • Only log warnings and errors by default.
  • Additional logs (for debugging/profiling) should be toggleable via flags/settings.
note

Recommendation: Use the built-in NestJS Logger.

Services vs. Controller/Resolver

  • Controller/Resolver: Represent the HTTP or GraphQL layer.
  • Service: Represents the business logic layer.

Service Best Practices

  • Services should be decoupled from the HTTP/GraphQL layer (e.g., avoid using InputTypes directly).
    • This allows them to be reused (e.g., in console jobs) without mocking types.
  • Prefer passing the entire entity rather than just the ID.
  • Services should be easily testable. (Using MikroORM with NestJS framework)
  • Use Dependency Injection (DI) to control access to services and enforce clear module boundaries/interfaces.

Service FAQs

  • Is a service always required?
    No — don’t create pass-through methods just for the sake of it.
    Yes — if business logic is reused or the HTTP layer becomes messy.

  • Can I create utility files?
    Yes, as long as they are stateless helper functions.
    Do not consume other services or repositories within them.

    note

    Give them specific names — avoid generic utils dump folders.

Access Control Lists (ACL) / Authorization Checks

  • Place ACL logic in a dedicated ACL service (e.g., products.acl.service.ts) or inline in the controller/resolver for specific permission checks.
  • Do not perform ACL checks in shared services: Console jobs, for instance, have no authenticated user and may lead to double-checking permissions.
  • ACLs should be covered by unit tests.

Avoid Using process.env Directly

Directly accessing environment variables in the code has downsides:

  • Not testable (no dependency injection)
  • No central place to manage environment variables
  • No validation

Exception: It's technically necessary in some cases, such as in app entry points (main.ts, bootstrap.ts).

Dependency Injection

Constructor-based injection is preferred over property-based injection (see: Documentation | NestJS - A progressive Node.js framework).
Use property injection only when technically necessary.

ORM / Database

Repository

Prefer repository.create over new Entity() since create expects a complete data object.

Migrations

Down migrations are not required – using throw new Error("Unsupported") is sufficient.

We do not use rollbacks; issues are resolved with a new deployment.

Data Types

  • Always use uuidv4 as the primary key whenever possible.
    • Prevent listing or guessing of IDs (e.g., when they are part of a URL).
    • Can be generated "offline" (without a connection to the database), making the ID available in the code before insertion.
    • Are globally unique, which can sometimes be helpful.
    • Use columnType: "uuid" for UUID columns.
  • Always use columnType: text for strings. Explanation: See Don't Do This - PostgreSQL wiki.
  • Prefer jsonb over json. Explanation: See 8.14. JSON Types
  • For timestamps, always use columnType: "timestamp with time zone".

DB Defaults

Avoid using DB defaults as they are not available before inserting/updating. This also falsifies the types of the entity.

Bad
@Property({ default: false })
@Field()
visible: boolean;
Good
@Property()
@Field()
visible: boolean;

(if GQL Field → provide default value for InputType)

Good
@Property()
visible: boolean = false;

(if not a GQL Field)

info

If you are using the Upsert-Pattern, you may use DB defaults additionally

@Field()
@Property({
columnType: "timestamp with time zone",
onUpdate: () => new Date(),
defaultRaw: "now()", // fallback for upsert
})
updatedAt: Date = new Date();

GraphQL

Enum Keys = Values

Enum keys and values must be identical.

Bad
enum Category {
MAIN = "main"
SECONDARY = "secondary"
}
Good
enum Category {
Main = "Main"
Secondary = "Secondary"
}

Reason: Admin and Site only know the GraphQL schema, where the enum keys are used. Therefore, queries and mutations also send the keys. If the values differ from the keys, this causes API errors because the wrong values are used for the enum.

Regarding naming conventions, see also TypeScript | Enums.

TypeScript number vs GraphQL Int/Float

TypeScript has only one number type, but GraphQL distinguishes between Int and Float, so the type must be specified manually, otherwise, Float is the less strict default.

class Product {
@Field(() => Int)
position: number;

@Field(() => Float)
price: number;
}