3.4 · SQL with Node: PostgreSQL, MySQL, Prisma

Level 3 · Express & databases

3.4SQL with Node: PostgreSQL, MySQL, Prisma

Goal: know how to persist with a relational database (PostgreSQL/MySQL) via a modern ORM (Prisma), writing safe queries — completing your mastery of both database families.
Estimated time: 11 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • Relational structures: tables, rows, foreign keys (PostgreSQL/MySQL), strict schema and ACID transactions for integrity.
  • SQL is written or generated: SELECT/INSERT/UPDATE/DELETE + JOIN as parameterised raw SQL, or ORM (Prisma/Sequelize) methods generating safe SQL.
  • Security is non-negotiable: never concatenate user data (SQL injection) — parameterised queries or ORM plus understanding of the underlying SQL.

Running case — Salim invoices in SQL flawlessly

Salim, junior developer in Algiers, must invoice: linked clients, orders and line items, totals to guarantee. Flexible NoSQL is no longer enough. He models three tables linked by foreign keys (PostgreSQL), writes his first parameterised SELECT/JOIN queries, then moves to Prisma: declarative schema.prisma schema, versioning migration, typed prisma.commande.create() and findMany() client. When a search field contains a trapped quote, parameterisation neutralises it: no injection.

Last episode of the Salim thread — from Express to both database families: complete persistence.

1. The relational model & SQL

The SQL / relational family (mainly PostgreSQL, powerful and open source, and MySQL/MariaDB) organises data into tables (typed columns), rows (records) and formal relations via foreign keys (an order references a client). The SQL language queries: SELECT (read), INSERT (create), UPDATE, DELETE, JOIN (combine linked tables). Strong guarantees: strict schema and ACID transactions for consistency — ideal for structured, critical data.

From Node, two levels: the raw driver (e.g. pg for PostgreSQL) where SQL is written in strings — with the imperative parameterised queries ($1, $2…) since concatenating user input opens SQL injection, a critical flaw. Or the ORM generating the SQL for you.

2. Prisma: schema, migrations, typed client

An ORM (Object-Relational Mapper) maps tables ↔ JS objects: you handle prisma.client.create({ data }) and findMany({ where }) instead of SQL strings — more readable, more productive, and protected from injections by automatic parameterisation. The common ones: Prisma (modern, highly regarded) and Sequelize (older, proven).

Prisma stands out with its declarative schema (schema.prisma file describing models and relations), its migrations (versioning schema evolution) and its generated, typed client (autocompletion, type safety — excellent with TypeScript, section 5-1). Recommended for most apps; keep raw SQL for very specialised queries. Salim’s habit: understand the generated SQL (load relations efficiently, avoid N+1) instead of treating the ORM as a black box.

Common misinterpretation: “With an ORM, no need to learn SQL, and concatenating input into the query does the job”. No. Without SQL basics (joins, relation loading), the ORM produces misunderstood, inefficient queries (N+1). And concatenating user data, even “just for testing”, opens an exploitable SQL injection: parameterisation always, with no exception.
SQL with Node: two levels From direct SQL to typed client RelationalTables, rows, foreign keys+Raw driverParameterised SQL, never concatenated+Prisma ORMSchema, migrations, typed client The ORM protects from injections; SQL remains to be understood.
From direct SQL to typed client
Text version of the diagram (SEO / screen readers / translation): 1) Relational: Salim links clients, orders and line items by foreign keys. 2) Raw driver: his SELECT/JOIN queries go through as parameterised queries. 3) ORM: Prisma generates a typed client from schema.prisma with migrations. Lesson: ORM productivity without giving up SQL.

🔎 Concrete example — Prisma: a typed, safe CRUD

Salim creates and lists clients without writing SQL.

// schema.prisma declares linked Client and Commande (1-N)
const nouveau = await prisma.client.create({
  data: { nom: 'Salim', email: 'salim@fictitious-example.test' }
});
const actifs = await prisma.client.findMany({
  where: { actif: true }
});
// Equivalent SAFE raw (pg) — never any concatenation:
// await pool.query('SELECT * FROM clients WHERE email = $1', [emailSaisi]);

Reading: objects and methods on the Prisma side, parameterised $1 on the raw side.

Lesson: both paths neutralise injection; concatenation, never.

Section vocabulary

SQL / relational
Databases in tables, rows and foreign keys (PostgreSQL, MySQL); strict schema and ACID transactions.

Simple example: Linked clients, orders and line items at Salim’s.

What it is not: flexible documents with no schema.

Reflex: choose it for linked, critical data.

SQL (language)
SELECT (read), INSERT (create), UPDATE, DELETE, JOIN (combine linked tables).

Simple example: Salim’s SELECT with commands-clients JOIN.

What it is not: ORM method calls.

Reflex: know how to read a SELECT/JOIN even with an ORM.

SQL injection
Critical flaw: concatenated input becomes executed code; prevented by parameterisation.

Simple example: A trapped quote neutralised by $1.

What it is not: a mere harmless display error.

Reflex: never concatenate, always parameterise.

ORM (Prisma, Sequelize)
Maps tables ↔ JS objects and generates SQL: readable, safe, productive methods.

Simple example: prisma.client.findMany({ where }) at Salim’s.

What it is not: hand-concatenated raw SQL.

Reflex: prefer the ORM for everyday CRUD.

Prisma (modern)
ORM with declarative schema (schema.prisma), versioned migrations and generated typed client.

Simple example: The migration adding the line-items table.

What it is not: a raw driver with no model or type.

Reflex: version each evolution with a migration.

In practice — Persist with a SQL database and an ORM

  1. Understand the relational model: tables, typed columns, rows, foreign-key relations; and basic SQL operations (SELECT, INSERT, UPDATE, DELETE, JOIN).
  2. Remember the security imperative: NEVER concatenate user data into SQL (injection) — parameterised queries or ORM.
  3. Discover a modern ORM: define a schema (Prisma: schema.prisma) and run a CRUD through methods (prisma.user.create/findMany/update/delete).
  4. Explore Prisma migrations (versioning schema evolution) and the value of the typed client (autocompletion, type safety with TypeScript).
You can persist with a relational database (PostgreSQL/MySQL) via a modern ORM (Prisma), writing safe queries — completing your mastery of both database families.

Ready-to-say script — presenting your safe SQL (30 s)

“Invoicing runs on PostgreSQL: clients, orders and line-item tables linked by foreign keys. CRUD goes through Prisma, declarative schema plus migrations, typed client. Remaining raw SQL is parameterised with $1, never concatenated: tested with a trapped input, the injection is neutralised.”

Say in security review; show the schema and one parameterised query.

Copy-paste template — Prisma + parameterised query

// schema.prisma (fictitious excerpt):
// model Client { id Int @id @default(autoincrement())
//   nom String
//   email String @unique
//   commandes Commande[] }
async function main() {
  const nouveau = await prisma.client.create({
    data: { nom: 'Salim', email: 'salim@fictitious-example.test' }
  });
  const liste = await prisma.client.findMany({ where: { actif: true } });
  await prisma.client.update({
    where: { email: 'salim@fictitious-example.test' },
    data: { actif: false }
  });
  // Equivalent SAFE raw (pg) — never any concatenation:
  // await pool.query('SELECT * FROM clients WHERE email = $1', [emailSaisi]);
}
main().catch(console.error);

Freelancer

Invoice in PostgreSQL + Prisma: three linked tables and one typed CRUD are enough.

SME in Algiers

Salim versions each column addition with a migration reviewed before deployment.

Product team

Enforce the shared typed client for consistent queries across teams.

Beginner

This week: one Prisma model, one create and one findMany with where.

Caution: SQL data and access: work on a local or test database with fictitious data (emails in @fictitious-example.test), never paste a password or real connection string into code or screenshots, and give each role minimum rights. Every user input goes as a parameter ($1, ORM), never concatenated. When in doubt — exposed secret or concatenated query — stop, purge the secret and regenerate it before continuing.

✓ Best practices

  • Use a modern ORM (Prisma) for productivity and security.
  • Always parameterised queries (never concatenation) against SQL injection.
  • Understand the underlying SQL and load relations efficiently (avoid N+1).

✗ Common pitfalls

  • Concatenating user data into a SQL query (critical injection).
  • Treating the ORM as a black box (inefficient queries, N+1).
  • Ignoring SQL basics while relying fully on the ORM.

Key takeaways

  • SQL / RELATIONAL (PostgreSQL, MySQL): data in TABLES/ROWS with FOREIGN-KEY relations; strong guarantees (ACID).
  • SQL language: SELECT/INSERT/UPDATE/DELETE + JOIN. CRITICAL security: never concatenate user data (SQL INJECTION).
  • An ORM (PRISMA, Sequelize) maps tables ↔ objects and generates SQL: readable, injection-safe, productive.
  • PRISMA: declarative schema + migrations + TYPED client. Mastering SQL + NoSQL = complete persistence toolbox.

48h challenge — apply tomorrow

Declare 1 Prisma model and run a create + findMany, then rewrite one SELECT as parameterised $1.

Section exit checklist
  • I name tables, foreign keys and ACID.
  • My raw SQL is parameterised, never concatenated.
  • My Prisma CRUD goes through schema + migration.
Self-rating 1-5: are my raw queries all parameterised?can I explain the SQL generated by my ORM?

Frequently asked questions

Raw SQL driver or ORM (Prisma/Sequelize): which to choose?

For most apps, an ORM: productivity (concise prisma.user.create()), security (automatic anti-injection parameterisation), readability and simple relations. The raw driver (pg) keeps its place for fine control and specialised queries. Salim combines: ORM daily, parameterised SQL for complex JOIN reports.

What is SQL injection and how to protect against it?

A concatenated user input in SQL can become executed code (auth bypass, data reading or destruction). Danger example: ' OR '1'='1 slipped into an email. Protection: parameterised queries ($1 placeholders separating code and data) and ORMs parameterising by default. Never concatenate, even in tests — fundamental security knowledge.

Check your understanding

Which practice protects against SQL injection in Node?

Additional resources (optional, to go further)

Level 3 examFresh, harder questions to test what you've learned