Yevhen Klymentiev
dark
light
console
darkness
y.klymentiev@gmail.com
Coding Standards|Rules, conventions, and best practices I follow and recommend to teams

Database & ORM

Establish clear, consistent, and secure practices for interacting with databases and ORMs.

These rules aim to prevent common pitfalls like SQL injection, improve performance and maintainability, and ensure predictable data access across the project.

Use parameterized queries or ORM abstractions

Avoid raw string interpolation when building queries. Always use parameterized queries or trusted ORM query builders (like Prisma, Knex, Sequelize) to prevent SQL/NoSQL injection.

TypeScript
Copied!
const query = `SELECT * FROM users WHERE email = '${email}'`;
TypeScript
Copied!
const user = await db.user.findUnique({ where: { email } });

Centralize database access logic

Avoid scattered DB calls across components or business logic. Instead, group queries and transactions in reusable services or repositories to promote testability, encapsulation, and single responsibility.

  • UserService.getByEmail(email)

  • AuthRepository.saveToken(token)

Normalize data when appropriate

Avoid unnecessary duplication of data unless denormalization is deliberate for performance. Proper normalization makes updates safer and avoids inconsistencies. Use relations and joins instead of duplicating related fields in multiple tables.

Note: Denormalization can be used in specific read-heavy use cases (e.g., analytics), but should be well-documented and intentional.

Use migrations for schema changes

Always track schema changes through version-controlled migrations instead of modifying the database manually. This guarantees consistency across environments (dev, staging, prod) and simplifies rollbacks.

Tools:

  • Prisma Migrate

  • Knex.js Migrations

  • Sequelize CLI

  • TypeORM Migrations

File
Copied!
npx prisma migrate dev --name add_user_profile

When multiple queries must succeed or fail together (e.g., creating user + profile), wrap them in a transaction to maintain consistency and avoid partial writes.

TypeScript
Copied!
1await prisma.$transaction([
2  prisma.user.create({ data: ... }),
3  prisma.profile.create({ data: ... }),
4]);

Be careful with lazy loading

Lazy loading can cause unintended N+1 query problems if not used carefully. Always assess whether eager loading is more appropriate for performance and readability, especially in loops or nested object access.

N+1 Problem

TypeScript
Copied!
1const posts = await prisma.post.findMany();
2for (const post of posts) {
3  const author = await prisma.user.findUnique({ where: { id: post.authorId } });
4}

Eager Loading

TypeScript
Copied!
1const posts = await prisma.post.findMany({
2  include: { author: true }
3});

Prefer eager loading for known relations

When you know you'll need related data, fetch it eagerly in a single query. It improves performance and makes the code easier to follow, especially for APIs and rendering views.

Use Cases:

  • Fetching post + author

  • Order + line items

  • User + roles

Validate incoming data before persisting

Never trust incoming data to be well-formed or safe. Always validate or sanitize input before persisting it to the database, even if the front end already performs validation.

  • Use zod, yup, class-validator, etc.

  • Add model-level constraints when supported (e.g. Prisma schema, DB constraints)

TypeScript
Copied!
1const schema = z.object({
2  email: z.string().email(),
3  age: z.number().int().min(0),
4});
5schema.parse(req.body);

Prefer soft deletes for important data

Instead of permanently deleting important records, use a deletedAt timestamp or a isDeleted flag. This approach allows data recovery, audit trails, and safer business logic.

File
Copied!
1model User {
2  id        Int       @id @default(autoincrement())
3  email     String    @unique
4  deletedAt DateTime? // null = active
5}

Always use pagination for large queries

Never return unbounded datasets in production. Always use pagination (offset/limit, cursor, or seek) to reduce server load, bandwidth usage, and improve performance.

TypeScript
Copied!
1const users = await prisma.user.findMany({
2  take: 20,
3  skip: 0,
4  orderBy: { createdAt: "desc" }
5});

Index frequently queried columns

Use indexes on columns that are frequently used in WHERE, ORDER BY, or JOIN clauses. It significantly speeds up query execution time, especially in large datasets.

Avoid over-indexing — every index slows down INSERT/UPDATE/DELETE.

File
Copied!
1model Post {
2  title     String
3  createdAt DateTime @default(now())
4  @@index([createdAt])
5}

Limit select fields when possible

Avoid selecting all columns unless necessary (SELECT *). Fetch only the required fields to reduce memory usage, improve performance, and protect sensitive data from accidental exposure.

TypeScript
Copied!
1const user = await prisma.user.findUnique({
2  where: { id },
3  select: { id: true, email: true }
4});

Enforce uniqueness at the DB level

Application-level checks aren’t enough — always enforce uniqueness at the schema or database level. Use UNIQUE constraints or equivalent ORM features to prevent race conditions and inconsistent states.

File
Copied!
1model User {
2  email String @unique
3}

Use aggregations carefully

Aggregation queries (e.g. COUNT, SUM, AVG) can be expensive — especially when used on large, unindexed datasets or in real-time APIs. Cache results or use background jobs where appropriate.

Good Practice:

  • Index grouped fields

  • Avoid aggregating frequently on write-heavy tables

  • Cache analytics where possible

Apply role-based access at the query layer

Don’t rely solely on frontend or route-level checks. Enforce permissions directly in the data access layer to prevent unauthorized data exposure — especially in multi-tenant applications or shared-resource models.

TypeScript
Copied!
1// Express + Prisma
2async function getInvoicesForUser(req, res) {
3  const { user } = req;
4
5  const whereClause =
6    user.role === "admin"
7      ? {}
8      : { organizationId: user.organizationId };
9
10  const invoices = await prisma.invoice.findMany({
11    where: whereClause,
12  });
13
14  res.json(invoices);
15}

This ensures that even if a user bypasses the frontend, they only get data allowed by their role or scope.

Styleguide: Database & ORM | Yevhen Klymentiev