<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mr. S Gupta</title>
    <description>The latest articles on DEV Community by Mr. S Gupta (@surajsrggupta).</description>
    <link>https://dev.to/surajsrggupta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035061%2F5c94ecc6-7f77-4e5d-b1d1-eff09aea4855.png</url>
      <title>DEV Community: Mr. S Gupta</title>
      <link>https://dev.to/surajsrggupta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/surajsrggupta"/>
    <language>en</language>
    <item>
      <title>How Express Actually Talks to a Database: From HTTP Request to SQL Query</title>
      <dc:creator>Mr. S Gupta</dc:creator>
      <pubDate>Sun, 30 Aug 2026 14:04:25 +0000</pubDate>
      <link>https://dev.to/surajsrggupta/how-express-actually-talks-to-a-database-from-http-request-to-sql-query-3n6l</link>
      <guid>https://dev.to/surajsrggupta/how-express-actually-talks-to-a-database-from-http-request-to-sql-query-3n6l</guid>
      <description>&lt;p&gt;When beginners start learning backend development, they usually pick up things one at a time and separately, Express routes here, controllers there, middleware somewhere else, then databases, then Prisma, then authentication. Each piece makes sense on its own.&lt;/p&gt;

&lt;p&gt;But one question tends to linger even after all that. When a user clicks something on a website, what's actually happening behind the scenes, step by step?&lt;/p&gt;

&lt;p&gt;Say someone opens an e-commerce site and clicks "Show my orders." What happens right after that click? Does Express talk to the database directly? Does Prisma just handle everything on its own? Where exactly does authentication slot in, and where does the actual SQL query get executed? Once this full flow actually clicks, backend development stops feeling like a pile of disconnected pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big picture
&lt;/h2&gt;

&lt;p&gt;A modern backend app generally looks something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend Application
        |
        v
HTTP Request
        |
        v
Express Server
        |
        v
Routes
        |
        v
Controllers
        |
        v
Services
        |
        v
Database Layer
        |
        v
PostgreSQL Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer here has its own job, so let's walk through them one at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The user sends a request.&lt;/strong&gt; Say someone clicks "View Profile." The frontend fires off an HTTP request, something like &lt;code&gt;GET /api/profile&lt;/code&gt;, carrying a method, a URL, maybe an authorization header, and sometimes a body. The browser genuinely has no idea your database even exists, it only ever talks to your backend API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The request reaches the Express server&lt;/strong&gt;, which is just sitting there running somewhere, waiting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment that &lt;code&gt;GET /api/profile&lt;/code&gt; request arrives, Express picks it up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The router figures out where it goes.&lt;/strong&gt; A real app has dozens of endpoints, &lt;code&gt;/api/users&lt;/code&gt;, &lt;code&gt;/api/products&lt;/code&gt;, &lt;code&gt;/api/orders&lt;/code&gt;, &lt;code&gt;/api/payments&lt;/code&gt;, and the router's whole job is deciding which piece of code handles which one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain terms, if someone hits &lt;code&gt;GET /profile&lt;/code&gt;, run the &lt;code&gt;getProfile&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Middleware runs before the controller ever sees the request.&lt;/strong&gt; Think of it as a checkpoint the request has to pass through first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
   ↓
Authentication Middleware
   ↓
Validation Middleware
   ↓
Controller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Say a request comes in with &lt;code&gt;Authorization: Bearer token123&lt;/code&gt;. The middleware's job is simply asking, is this token actually valid?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything checks out, &lt;code&gt;next()&lt;/code&gt; runs and the request keeps moving forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The controller takes over.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A controller's real job is just taking the request, pulling out what it needs, calling whatever service handles the actual work, and sending back a response. It really shouldn't be carrying heavy database logic itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The service layer handles the actual business logic.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the real database call happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prisma steps in here.&lt;/strong&gt; When your code calls &lt;code&gt;prisma.user.findUnique()&lt;/code&gt;, Prisma quietly builds the actual SQL behind it, something like &lt;code&gt;SELECT * FROM users WHERE id = 1;&lt;/code&gt;, and PostgreSQL runs it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service
   ↓
Prisma
   ↓
SQL Query
   ↓
PostgreSQL
   ↓
Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The database sends data back&lt;/strong&gt;, something like &lt;code&gt;{ "id": 1, "name": "Rahul", "email": "rahul@test.com" }&lt;/code&gt;, and it travels all the way back up through Prisma, the service, the controller, the Express response, and finally lands back at the frontend.&lt;/p&gt;

&lt;p&gt;Put the whole thing together and you get this complete picture, from the click all the way to the UI updating.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Clicks Button
        |
Frontend Sends HTTP Request
        |
Express Server
        |
Router
        |
Middleware
        |
Controller
        |
Service
        |
Prisma / Drizzle / SQL
        |
PostgreSQL
        |
Response Returns
        |
Frontend Updates UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A question that comes up a lot at this point is, why not just call the database straight from the controller? Technically, you absolutely can.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That'll work fine for a small project. The trouble shows up once the app grows, controllers stacked directly on the database without any layers in between tend to balloon in size, logic gets duplicated all over the place, testing becomes a pain, and maintaining any of it gets genuinely difficult over time. With proper layering, routes to controllers to services to a dedicated database layer, code stays organized, testing gets easier, teams can actually work in parallel without stepping on each other, and future changes stay contained instead of rippling everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does all this code actually live
&lt;/h2&gt;

&lt;p&gt;Knowing the flow is one thing, but figuring out where this code should actually sit inside a real project is the next question worth answering.&lt;/p&gt;

&lt;p&gt;Beginners typically start with something dead simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src
├── index.ts
├── routes.ts
└── database.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's genuinely fine for a small project. But as users pile up, products get added, payments show up, authentication gets bolted on, keeping everything crammed into a couple of files stops being manageable.&lt;/p&gt;

&lt;p&gt;A more production style layout tends to look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src
├── index.ts
├── app.ts
├── config
│   └── env.ts
├── routes
│   ├── user.routes.ts
│   └── product.routes.ts
├── controllers
│   ├── user.controller.ts
│   └── product.controller.ts
├── services
│   ├── user.service.ts
│   └── product.service.ts
├── repositories
│   ├── user.repository.ts
│   └── product.repository.ts
├── db
│   └── prisma.ts
├── middleware
│   ├── auth.ts
│   └── error.ts
├── validators
└── utils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every folder here is pulling its own weight. &lt;code&gt;index.ts&lt;/code&gt; is really just the entry point, its only job is starting the server, nothing more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app.ts&lt;/code&gt; is where the Express app actually gets configured, middleware loaded, routes wired up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;userRoutes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./routes/user.routes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userRoutes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The routes layer's whole job is deciding which URL goes to which controller, nothing about the database belongs here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller layer handles request and response, pulling data out and handing it off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service layer is where the actual business logic lives, deciding what needs to happen, in what order, and under what rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository layer is what talks directly to the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the real payoff of splitting things this way. If Prisma ever got swapped out for Drizzle down the line, only the repository layer would need to change, the service code sitting above it wouldn't need to know or care. That's really the whole point of the repository pattern.&lt;/p&gt;

&lt;p&gt;And the database layer itself is usually just one shared client instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's worth flagging why this matters, because beginners sometimes create a fresh Prisma client inside every single request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a genuinely bad habit, since it can spin up way too many database connections and hurt performance badly. Sticking to a single shared instance, sitting on top of a proper connection pool, is the better approach.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
   ↓
Single Prisma Client
   ↓
Database Connection Pool
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A login flow is a good example of how all these layers cooperate. A &lt;code&gt;POST /login&lt;/code&gt; request moves through the route, into the controller, into an auth service, which hits the database, checks the password, generates a JWT, and finally sends back a response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User input should never head straight to the database untouched either. Something like &lt;code&gt;{ "name": "", "email": "wrong-email" }&lt;/code&gt; needs to pass through a validation layer first, usually something like Zod, Joi, or Yup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Errors deserve real handling too, not just a stray &lt;code&gt;console.log(error)&lt;/code&gt; buried in a try-catch. A cleaner approach routes errors through dedicated middleware instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put together, the full architecture looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   ↓
Routes
   ↓
Controllers
   ↓
Services
   ↓
Repositories
   ↓
Prisma / Drizzle
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this is mandatory for every project though. A small app can genuinely get by with just routes, controllers, a db folder, and an entry file. Services and repositories are things you layer in as the project actually grows into needing them, not something you're required to bolt on from day one. The real goal here was never piling on more folders for their own sake, it's making sure every piece has a clear, single responsibility. If the database ever needs to change, the whole app shouldn't need rewriting. If auth logic changes, only auth related code should need touching. That's really what a maintainable backend comes down to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually happening between Express, the ORM, and the database
&lt;/h2&gt;

&lt;p&gt;There's still one layer worth pulling apart properly, what's actually going on inside that database layer. When you write &lt;code&gt;await prisma.user.findMany()&lt;/code&gt; or &lt;code&gt;await db.select().from(users)&lt;/code&gt;, what actually reaches the database? Does Prisma talk to PostgreSQL directly? How does a connection even get established? And what happens when a hundred people hit your API at the same exact moment?&lt;/p&gt;

&lt;p&gt;The flow isn't quite as direct as beginners often assume. It's not just Express straight to database, it's really more like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Express Application
   ↓
Database Client / ORM
   ↓
Database Driver
   ↓
Database Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or more concretely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Express
   ↓
Prisma Client
   ↓
PostgreSQL Driver
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A database driver is really just the software layer handling the actual back and forth between your app and the database. Connecting Node.js to PostgreSQL usually starts with installing the &lt;code&gt;pg&lt;/code&gt; package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That driver's job is opening connections, sending queries, and handing results back. Prisma doesn't reinvent PostgreSQL's wire protocol itself, it sits on top of a driver underneath it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Code
   ↓
Prisma
   ↓
Database Driver
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;await prisma.user.findMany()&lt;/code&gt; becomes &lt;code&gt;SELECT * FROM users;&lt;/code&gt; under the hood, and the driver is what actually carries that query over to the database.&lt;/p&gt;

&lt;p&gt;When your app first starts up, the database client gets initialized, though the actual connection usually isn't opened right away, it tends to get established only when it's actually needed, sometimes called a lazy connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server Start
   ↓
Database Client Initialize
   ↓
Connection Available
   ↓
Requests Accept
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where connection pooling becomes a genuinely important concept. Picture a hundred users hitting your API at once. If every single request tried spinning up a brand new database connection from scratch, the database would get overwhelmed pretty fast. That's exactly what connection pools exist to prevent, a small set of ready-to-go connections sitting in a pool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
   ↓
Connection Pool
[Connection 1] [Connection 2] [Connection 3]
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request comes in, grabs whatever connection's free, does its work, and hands that connection back to the pool once it's done. Say your pool caps out at 10 connections and 100 users show up at once, the first 10 requests grab connections right away, and everyone else just waits their turn until one frees up.&lt;/p&gt;

&lt;p&gt;Development and production databases also tend to look pretty different day to day. Locally you're often just running something like PostgreSQL in a Docker container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, you're usually looking at a managed PostgreSQL service instead, something like AWS RDS, Neon, Supabase's PostgreSQL offering, or Railway.&lt;/p&gt;

&lt;p&gt;And database credentials should never be hardcoded directly into your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't do this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres://user:password@localhost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's both a security risk and a deployment headache waiting to happen. Environment variables are the better home for this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql://user:password@host/database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracing a real request end to end makes all of this click. Say a &lt;code&gt;GET /api/users/10&lt;/code&gt; request comes in. Express routes it through &lt;code&gt;router.get("/users/:id", getUser)&lt;/code&gt;. The controller pulls the id out of the params and calls the service. The service calls the repository. The repository calls &lt;code&gt;prisma.user.findUnique({ where: { id } })&lt;/code&gt;, which Prisma turns into &lt;code&gt;SELECT * FROM users WHERE id=10;&lt;/code&gt;. PostgreSQL runs that, finds the row, and hands it back. From there it travels back up through Prisma, the repository, the service, the controller, and out through Express to the browser as something like &lt;code&gt;{ "id": 10, "name": "Rahul", "email": "rahul@test.com" }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Validation and authentication both slot in before the database ever gets touched. Registration data gets checked by something like Zod before it's allowed anywhere near the database. Protected routes run through JWT middleware first, and if that token's invalid, the database query simply never executes at all.&lt;/p&gt;

&lt;p&gt;None of this is just academic trivia either. If you only ever memorize Prisma syntax or Express routing without understanding this flow, you can absolutely build things that work. But the moment something actually breaks, a slow query, a failing database connection, memory creeping up, a sluggish API response, that's exactly when understanding this architecture actually pays off. Backend development was never really about memorizing syntax. It's about understanding the journey a request takes from start to finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling it all together
&lt;/h2&gt;

&lt;p&gt;Putting the entire journey in one place looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request
   ↓
Express Server
   ↓
Route
   ↓
Middleware
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
ORM / Database Driver
   ↓
Database
   ↓
Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or laid out as a full production style diagram.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     Client
                       |
                       v
                HTTP Request
                       |
                       v
                Express Server
                       |
                       v
                   Router
                       |
                       v
                Middleware Layer
          (Auth, Validation, Logging)
                       |
                       v
                  Controller
                       |
                       v
                   Service
              (Business Logic)
                       |
                       v
                 Repository
              (Database Logic)
                       |
                       v
            Prisma / Drizzle / SQL
                       |
                       v
                PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lot of beginners end up wondering whether the API itself is basically the database. It's not, an API is really just a communication layer. The frontend asks for something, &lt;code&gt;GET /api/products&lt;/code&gt; say, and the backend decides whether that user's allowed to see it, what data actually needs fetching, what query the database needs, and how the response should be shaped. The API's really just acting as the messenger in between.&lt;/p&gt;

&lt;p&gt;And there's a good reason the frontend never talks to the database directly, even though some databases technically allow it. Exposing credentials directly to the frontend is a straightforward security risk, since anyone poking around the client code could grab them. Business rules also need somewhere to live, checking stock availability, verifying payment completion, making sure a user isn't banned, and none of that belongs sitting in the frontend. And directly exposing a production database to the open internet is just asking for trouble. The safer shape is always frontend to backend API to database, never frontend straight to database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that tend to show up along the way
&lt;/h2&gt;

&lt;p&gt;Cramming everything into the controller is a really common one early on, validating users, checking products, calculating prices, updating the database, sending emails, building the response, all crammed into one route handler. It works at first, but that controller balloons into a thousand line mess pretty quickly on a bigger project. Splitting things into controller, service, and repository keeps that from happening.&lt;/p&gt;

&lt;p&gt;Scattering database queries across multiple controllers is another one, &lt;code&gt;prisma.user.findMany()&lt;/code&gt; showing up in one controller file and again somewhere else entirely. That spreads database logic all over the codebase instead of keeping it contained inside a proper repository layer.&lt;/p&gt;

&lt;p&gt;Treating the ORM like it's pure magic is a subtler trap. &lt;code&gt;prisma.user.findMany()&lt;/code&gt; looks effortless on the surface, but underneath it's still just running &lt;code&gt;SELECT * FROM users;&lt;/code&gt;. If the underlying SQL never actually makes sense to you, diagnosing real performance issues later becomes genuinely hard.&lt;/p&gt;

&lt;p&gt;Ignoring proper error handling is another common gap, and in production, things will go wrong eventually, a database going down, bad input, network hiccups, unauthorized access attempts, all of it needs a proper path through central error handling middleware rather than getting quietly swallowed.&lt;/p&gt;

&lt;p&gt;And hardcoding secrets directly into code, things like &lt;code&gt;const password = "123456"&lt;/code&gt;, is a habit worth killing early. Environment variables exist specifically so things like &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;JWT_SECRET&lt;/code&gt;, and &lt;code&gt;API_KEY&lt;/code&gt; never end up sitting in plain code.&lt;/p&gt;

&lt;p&gt;Applications also change shape over time, a users table might start with just id, name, and email, then later pick up a phone number and a created_at timestamp. Migrations exist to track exactly these kinds of structural changes over time, tools like Prisma Migrate, Drizzle Kit, Flyway, or Liquibase are all essentially version control for your database's shape.&lt;/p&gt;

&lt;p&gt;Good backends also tend to test layer by layer, controller tests checking whether the response looks right, service tests checking whether the business rules actually hold, repository tests checking whether the database queries themselves behave correctly. And logging matters a lot more once something's actually live, tools like Pino or Winston help capture things like request paths, status codes, and response times so debugging in production isn't a total guessing game. On top of that, keeping an eye on API response times, database performance, error rates, and general server health is just part of running something in production responsibly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a solid backend developer actually needs to understand
&lt;/h2&gt;

&lt;p&gt;Realistically, it comes down to a handful of layers stacking on top of each other. HTTP basics, requests, responses, headers, status codes. Express itself, routes, middleware, controllers. The database, SQL, relationships, indexes, transactions. The ORM sitting on top of that, Prisma, Drizzle, migrations. The overall architecture tying it together, services, repositories, proper error handling. And finally, production concerns, deployment, logging, security.&lt;/p&gt;

&lt;p&gt;A reasonable learning order tends to look like this: get comfortable with JavaScript or TypeScript fundamentals like async/await, promises, modules, and types. Then pick up HTTP basics, REST conventions, methods, status codes, headers. From there, build actual things with Express, simple APIs, middleware, basic auth. Then move into PostgreSQL properly, tables, relationships, joins, indexes. After that, bring in an ORM, Prisma first, then Drizzle. And finally, round it out with production concerns, Docker, deployment, logging, security.&lt;/p&gt;

&lt;p&gt;Honestly, the easiest way to hold all of this in your head is to think of a backend like a restaurant. The frontend user is the customer. The API is the waiter. The business logic is the chef. The database is the kitchen. And the application's rules are basically the recipe everyone's following. A customer never walks straight into the kitchen themselves, there's a whole system standing between them and the food, and a backend works exactly the same way, frontend to API to business logic to database.&lt;/p&gt;

&lt;p&gt;At the end of the day, the real skill in backend development was never memorizing one specific framework. Express, Prisma, Drizzle, PostgreSQL, these are all just tools. What actually matters is understanding how a request travels, how data gets stored, where business rules actually belong, how database queries get executed under the hood, and how to keep an architecture maintainable as it grows. Once those ideas are genuinely clear, picking up whatever new backend tool shows up next stops being intimidating, because at that point you're not just learning a tool, you're recognizing a system you already understand.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>dbms</category>
      <category>database</category>
      <category>express</category>
    </item>
    <item>
      <title>Prisma vs Drizzle vs Raw SQL: What Every Backend Developer Should Understand Before Choosing an ORM</title>
      <dc:creator>Mr. S Gupta</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:09:05 +0000</pubDate>
      <link>https://dev.to/surajsrggupta/prisma-vs-drizzle-vs-raw-sql-what-every-backend-developer-should-understand-before-choosing-an-orm-11ch</link>
      <guid>https://dev.to/surajsrggupta/prisma-vs-drizzle-vs-raw-sql-what-every-backend-developer-should-understand-before-choosing-an-orm-11ch</guid>
      <description>&lt;p&gt;When most developers start building backend applications, they're usually focused on one thing, getting an API working. Express routes, controllers, some auth, a bit of middleware, that's usually the whole mental model in the beginning.&lt;/p&gt;

&lt;p&gt;But sooner or later, a bigger question shows up. How exactly should your application be talking to the database?&lt;/p&gt;

&lt;p&gt;And that's when a bunch of unfamiliar words start floating around. Raw SQL, database drivers, ORMs, Prisma, Drizzle, query builders. Some people will tell you to never touch an ORM and just write SQL yourself. Others will swear Prisma made their life ten times easier. And then there's a smaller crowd that prefers Drizzle specifically because it stays close to SQL instead of hiding it.&lt;/p&gt;

&lt;p&gt;So what's actually going on here? Why do all these tools even exist, and how do you know which one to reach for? Let's build this up from the ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, understand how data actually moves
&lt;/h2&gt;

&lt;p&gt;Before Prisma or Drizzle mean anything to you, it helps to see the full picture of how a request travels through a backend app.&lt;/p&gt;

&lt;p&gt;Say a user opens your site and clicks something like "show my profile." That click travels roughly like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser

   |
   v

Express API

   |
   v

Database Layer

   |
   v

PostgreSQL Database

   |
   v

Response back to the user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "database layer" bit in the middle is where all the confusion tends to live. Your app needs some way to actually talk to PostgreSQL, and there are basically three common ways to do that: writing raw SQL yourself, using an ORM like Prisma, or using a query builder like Drizzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing raw SQL directly
&lt;/h2&gt;

&lt;p&gt;This is the oldest, most direct route there is, you just write the SQL yourself.&lt;/p&gt;

&lt;p&gt;Say you've got a users table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users

id
name
email
password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finding a user is as simple as this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database speaks SQL natively, so there's no translation happening anywhere.&lt;/p&gt;

&lt;p&gt;In Node.js, if you're on PostgreSQL, the &lt;code&gt;pg&lt;/code&gt; package is the usual way to send queries like that directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM users WHERE id = $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your code sends SQL, PostgreSQL runs it, and the data comes straight back. Nothing hidden, nothing abstracted away.&lt;/p&gt;

&lt;p&gt;Writing SQL yourself gives you complete control, obviously. You can write things like this without fighting any abstraction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For gnarly reports like that, SQL is genuinely hard to beat. Writing it yourself also forces you to actually understand joins, indexes, execution plans, and performance in general, which is knowledge that pays off no matter what tool you end up using later. And there's no extra layer sitting between your code and the database either.&lt;/p&gt;

&lt;p&gt;But raw SQL has real downsides once a project grows past a handful of queries. Imagine a codebase with hundreds of them scattered around.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="s2"&gt;`
SELECT *
FROM users
WHERE email=$1
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine someone renames a column somewhere down the line, say &lt;code&gt;email&lt;/code&gt; becomes &lt;code&gt;email_address&lt;/code&gt;. Every query touching that column is now quietly broken, and the database knows about the change immediately. Your TypeScript code has no idea until something crashes at runtime.&lt;/p&gt;

&lt;p&gt;There's a sneakier version of this problem too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spot the typo, &lt;code&gt;emial&lt;/code&gt; instead of &lt;code&gt;email&lt;/code&gt;. JavaScript won't say a word about it. You only find out once that line actually executes and blows up. This exact pain point is basically why ORMs exist in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is an ORM, really
&lt;/h2&gt;

&lt;p&gt;ORM stands for Object Relational Mapping, which sounds fancier than it actually is. All it really means is that there's a bridge sitting between your programming language and your database.&lt;/p&gt;

&lt;p&gt;Instead of writing this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You end up writing something closer to this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ORM takes what you wrote and turns it into actual SQL behind the scenes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your TypeScript code

        |
        v

ORM

        |
        v

SQL query

        |
        v

Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers reach for ORMs because modern apps need more than just sending queries around. They need type safety, proper migrations, schema management that doesn't require memorizing every table by hand, and honestly, just less repetitive boilerplate. That's the gap ORMs are trying to close.&lt;/p&gt;

&lt;p&gt;In the TypeScript world today, the names that come up most are Prisma, Drizzle, TypeORM, and Sequelize. This piece focuses mainly on raw SQL, Prisma, and Drizzle, since those three are what most modern TypeScript backend projects are actually choosing between right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting into Prisma
&lt;/h2&gt;

&lt;p&gt;Prisma is easily one of the most talked about ORMs in the TypeScript world, and it's usually one of the first names that comes up in any conversation about modern Node.js backends. So what problem is it actually solving?&lt;/p&gt;

&lt;p&gt;At its core, Prisma lets your TypeScript app talk to a database using regular TypeScript code instead of SQL scattered everywhere. It supports PostgreSQL, MySQL, SQLite, SQL Server, and MongoDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeScript code

        |
        v

Prisma Client

        |
        v

SQL queries

        |
        v

Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You write TypeScript, Prisma turns it into the actual queries.&lt;/p&gt;

&lt;p&gt;A typical Express plus Prisma plus PostgreSQL setup ends up looking roughly like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client

  |
  v

Express API

  |
  v

Service Layer

  |
  v

Prisma Client

  |
  v

PostgreSQL Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma Client is really the piece doing all the work here, it's the actual bridge between your backend code and the database sitting behind it.&lt;/p&gt;

&lt;p&gt;Everything starts with a file called &lt;code&gt;schema.prisma&lt;/code&gt;, which defines your entire database structure in one place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;       &lt;span class="nx"&gt;Int&lt;/span&gt;    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="nb"&gt;String&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;    &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;unique&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's basically saying, create a &lt;code&gt;User&lt;/code&gt; table with these four columns, and Prisma understands exactly what that means.&lt;/p&gt;

&lt;p&gt;Where raw SQL would need something like this to create the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma just needs the schema block shown above, and it handles turning that into actual database changes through something called a migration.&lt;/p&gt;

&lt;p&gt;A migration is really just a recorded history of database changes over time. Say your User table starts out simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User

id
name
email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Months later you decide to add a phone number field. Instead of manually altering the table yourself, you run something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prisma migrate dev &lt;span class="nt"&gt;--name&lt;/span&gt; add_phone_number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma generates a migration file to track that change, something like a folder with &lt;code&gt;001_initial_setup&lt;/code&gt; followed by &lt;code&gt;002_add_phone_number&lt;/code&gt;, so your entire database history stays saved and versioned instead of living only in someone's memory.&lt;/p&gt;

&lt;p&gt;Once your schema's defined, Prisma generates a client you actually use in your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, that quietly becomes this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You never had to type that SQL yourself.&lt;/p&gt;

&lt;p&gt;The common operations map over pretty intuitively too. Creating a record that would normally be an &lt;code&gt;INSERT INTO users (name, email) VALUES (...)&lt;/code&gt; becomes &lt;code&gt;prisma.user.create({ data: { name, email } })&lt;/code&gt;. Fetching all users, which would be a plain &lt;code&gt;SELECT * FROM users&lt;/code&gt;, becomes &lt;code&gt;prisma.user.findMany()&lt;/code&gt;. Updating a record swaps &lt;code&gt;UPDATE users SET name='Aman' WHERE id=1&lt;/code&gt; for &lt;code&gt;prisma.user.update({ where: { id: 1 }, data: { name: "Aman" } })&lt;/code&gt;. And deleting swaps &lt;code&gt;DELETE FROM users WHERE id=1&lt;/code&gt; for &lt;code&gt;prisma.user.delete({ where: { id: 1 } })&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Where Prisma really earns its popularity is type safety. If your schema defines a &lt;code&gt;User&lt;/code&gt; with &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt;, then writing something like this actually gives you working autocomplete on every field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you accidentally typo it as &lt;code&gt;user[0].emali&lt;/code&gt;, Prisma paired with TypeScript will flag that before your app even runs, which saves a genuinely surprising amount of debugging time down the line.&lt;/p&gt;

&lt;p&gt;Real applications almost never have just one table either. Say a user has many posts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User

id
name

Post

id
title
userId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma represents that relationship directly in the schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

 &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;

 &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

 &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

 &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;

 &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt;

 &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
 &lt;span class="nx"&gt;references&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And fetching a user along with their posts becomes a single readable call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="p"&gt;},&lt;/span&gt;
 &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
  &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which gives you back something like a user object with a nested posts array, exactly the shape you'd want to work with in your app.&lt;/p&gt;

&lt;p&gt;Prisma's biggest strengths really come down to how natural the developer experience feels, how strong the type safety is especially in TypeScript projects, how organized migrations become, how solid the documentation is, and how easily a whole team can look at the schema and immediately understand the database structure.&lt;/p&gt;

&lt;p&gt;It's not without trade-offs though. Genuinely complex reporting queries can start feeling awkward compared to just writing raw SQL. Because Prisma hides SQL so effectively, developers who only ever learn Prisma can end up struggling later when they actually need to optimize something at the database level. And there's technically an extra layer sitting in the request flow, application to Prisma to SQL to database, instead of the more direct application to SQL to database path. For most apps that extra layer is a complete non issue, but it's worth knowing it's there.&lt;/p&gt;

&lt;p&gt;Prisma tends to make the most sense when you're building APIs in TypeScript, want fast development, mostly need standard CRUD operations, and care a lot about developer experience. A lot of startups and SaaS products lean on it for exactly these reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Drizzle fits into the picture
&lt;/h2&gt;

&lt;p&gt;Drizzle is the other ORM that's picked up serious momentum in the TypeScript world, but understanding it really comes down to understanding one thing first, Prisma and Drizzle are built on genuinely different philosophies.&lt;/p&gt;

&lt;p&gt;Prisma's whole approach is keeping developers away from database complexity as much as possible. Drizzle takes the opposite stance, keep developers close to SQL, just give them TypeScript's safety on top of it. Both are chasing the same end goal, a better development experience around your database, they just take very different roads to get there.&lt;/p&gt;

&lt;p&gt;Drizzle is a lightweight TypeScript ORM that works with PostgreSQL, MySQL, and SQLite, and its whole focus is type safety, performance, staying close to SQL, and keeping the architecture lightweight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeScript code

        |
        v

Drizzle ORM

        |
        v

SQL query

        |
        v

Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The philosophy difference is easiest to see side by side. Fetching all users in Prisma looks like this, and you never really need to think about what SQL it's generating underneath.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drizzle asks you to write something noticeably closer to actual SQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usersTable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can basically read what's happening at the database level just by looking at the code.&lt;/p&gt;

&lt;p&gt;Schemas work differently too. Where Prisma defines things in its own &lt;code&gt;schema.prisma&lt;/code&gt; file, Drizzle defines the schema directly in TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="nx"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="nx"&gt;varchar&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/pg-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

 &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

 &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

 &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So your database structure literally lives inside your TypeScript codebase, no separate schema language to learn.&lt;/p&gt;

&lt;p&gt;The common operations feel like a TypeScript flavored version of SQL itself. Inserting a row that would be &lt;code&gt;INSERT INTO users (name, email) VALUES (...)&lt;/code&gt; in SQL becomes &lt;code&gt;db.insert(users).values({ name, email })&lt;/code&gt; in Drizzle. Fetching everything, which is &lt;code&gt;SELECT * FROM users&lt;/code&gt;, becomes &lt;code&gt;db.select().from(users)&lt;/code&gt;. And filtering, which would be &lt;code&gt;SELECT * FROM users WHERE id=1&lt;/code&gt;, becomes &lt;code&gt;db.select().from(users).where(eq(users.id, 1))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;People gravitate toward Drizzle for a handful of reasons. It's genuinely lightweight with very little unnecessary abstraction sitting in the way. Because it stays close to SQL, your actual SQL knowledge stays sharp and keeps growing rather than getting hidden behind an abstraction. The queries it generates tend to be predictable since there's less machinery translating your intent. And it was built with modern runtimes in mind too, things like edge environments and serverless setups.&lt;/p&gt;

&lt;p&gt;That said, if you've never really learned SQL, Drizzle can feel a lot more confusing upfront than Prisma does, since Prisma is generally the easier on-ramp for total beginners. Using Drizzle well also genuinely requires understanding joins, relations, indexes, and how queries actually work, there's no hiding from that. And its ecosystem, documentation, and community, while solid, are still smaller than Prisma's more mature setup.&lt;/p&gt;

&lt;p&gt;The simplest way to frame the difference is this. With Prisma you treat the database like a bunch of objects, &lt;code&gt;prisma.user.findMany()&lt;/code&gt;, and the whole focus is developer experience. With Drizzle you treat the database more like SQL itself, &lt;code&gt;db.select().from(users)&lt;/code&gt;, and the focus shifts toward performance and control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking at all three side by side
&lt;/h2&gt;

&lt;p&gt;At this point we've covered three real approaches, raw SQL, Prisma, and Drizzle, and architecturally they stack up like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw SQL:      Application → SQL Query → Database
Prisma:       Application → Prisma Client → SQL → Database
Drizzle:      Application → Drizzle → SQL → Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take one simple operation, finding a user by email, given a basic users table with &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt;. Here's how each approach handles the exact same task.&lt;/p&gt;

&lt;p&gt;Raw SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="s2"&gt;`
SELECT *
FROM users
WHERE email=$1
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rahul@test.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;

&lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
 &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rahul@test.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drizzle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rahul@test.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three get you the exact same data back at the end of the day. What actually differs is how much control you want, how much abstraction you're comfortable with, and how much SQL you already know going in.&lt;/p&gt;

&lt;p&gt;A mistake a lot of beginners make here is fixating on "which one's the fastest," when in real projects that's rarely the question that actually matters. The better questions are usually things like what the team's already comfortable with, how complex the project genuinely is, how strong everyone's database knowledge actually is, and how complicated the queries are likely to get. A simple SaaS app can usually get by just fine on Prisma alone. A database heavy application is where Drizzle, or even raw SQL in places, starts earning its keep.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Raw SQL&lt;/th&gt;
&lt;th&gt;Prisma&lt;/th&gt;
&lt;th&gt;Drizzle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Harder&lt;/td&gt;
&lt;td&gt;Easier&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Control&lt;/td&gt;
&lt;td&gt;Highest&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type Safety&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer Experience&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex Queries&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beginner Friendly&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  So what should you actually choose
&lt;/h2&gt;

&lt;p&gt;Should you always default to using an ORM? No, honestly. An ORM is a tool like any other, and no single tool is the right call for every project out there. Some apps genuinely move faster with one, others are better off without.&lt;/p&gt;

&lt;p&gt;Raw SQL earns its place when you need maximum control over the database, when the app is genuinely database heavy, when queries get complex, or when performance really can't take a hit. Think analytics platforms, reporting systems, financial applications, or anything doing heavy data processing. Say you need a report answering something like "how much has each customer purchased over the last five years, and what's their average order value." That kind of query is just naturally easier to express directly in SQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;

&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;

&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;

&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's really no substitute for SQL's raw power in cases like that. The catch is that leaning entirely on raw SQL across a large codebase gets unwieldy fast, hundreds of scattered query files, and you're left manually managing type safety yourself on top of it all.&lt;/p&gt;

&lt;p&gt;Prisma tends to be the better call when you're building a TypeScript backend, doing mostly standard CRUD work, care about development speed, and have multiple developers working across the same codebase. Think SaaS products, admin dashboards, e-commerce APIs, or content management systems, basically anywhere operations like creating a user, updating a profile, creating an order, or pulling dashboard data are the daily bread and butter. Prisma genuinely boosts productivity here, turning something like a raw &lt;code&gt;SELECT * FROM users WHERE id=1&lt;/code&gt; into a clean, readable &lt;code&gt;prisma.user.findUnique({ where: { id: 1 } })&lt;/code&gt; that any teammate can understand at a glance.&lt;/p&gt;

&lt;p&gt;Drizzle earns its spot when you're working in TypeScript, already know SQL reasonably well, care a lot about performance and control, and want something lightweight rather than heavy handed. It tends to appeal to developers who want the safety net of an ORM without feeling like they've been pulled too far away from the actual database.&lt;/p&gt;

&lt;p&gt;In practice, plenty of real projects don't stick to just one approach. A startup might reasonably begin like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Express → Prisma → PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is a genuinely practical starting point. As the app grows and analytics or complex reporting needs pile up, it's common to start layering raw SQL into specific parts of the app rather than ripping out Prisma entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
├── Prisma
└── Raw SQL Queries
        ↓
   PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using one approach everywhere isn't some rule you're required to follow, and most real world codebases end up mixing them anyway. Startups often lean toward TypeScript plus Prisma plus PostgreSQL purely for development speed. Newer, more modern TypeScript projects increasingly reach for Drizzle instead, valuing that lightweight, SQL friendly approach. And larger scale systems often end up combining an ORM with raw SQL and dedicated database optimization work, simply because different parts of a large system run into genuinely different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes worth avoiding
&lt;/h2&gt;

&lt;p&gt;A few things trip people up consistently here. The first is treating the ORM itself as the database. Prisma isn't a database, Drizzle isn't a database, they're just tools for accessing one, whether that's PostgreSQL, MySQL, or MongoDB sitting underneath.&lt;/p&gt;

&lt;p&gt;The second is skipping SQL entirely and jumping straight to Prisma. It feels productive early on, but it tends to backfire later when slow queries stop making sense, joins feel needlessly confusing, and database optimization becomes genuinely hard because there's no real foundation underneath the abstraction. An ORM was never meant to replace SQL, it's a layer sitting on top of it.&lt;/p&gt;

&lt;p&gt;The third is chasing raw performance numbers above everything else. Developer productivity matters just as much in the real world. Shaving five milliseconds off a query while making development three months slower isn't automatically the smarter trade to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  A learning path worth following
&lt;/h2&gt;

&lt;p&gt;If starting from zero today, this is roughly the order worth going in. Start with SQL fundamentals, tables, primary keys, foreign keys, joins, indexes, transactions, all using something like PostgreSQL. Then spend some real time connecting directly with something like Node's &lt;code&gt;pg&lt;/code&gt; package, so you actually understand how an application talks to a database without anything hidden in between. From there, move into Prisma, get comfortable with schemas, migrations, relations, and the type safety it brings. And finally, give Drizzle a proper try, so you understand what an SQL-first approach with a query builder actually feels like, along with the performance trade-offs that come with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves you
&lt;/h2&gt;

&lt;p&gt;The point of an ORM was never to replace SQL. It's there to make the developer experience better, nothing more grand than that.&lt;/p&gt;

&lt;p&gt;A genuinely strong backend developer isn't someone who just happens to know Prisma or Drizzle well. It's someone who understands how a database actually works, how to write SQL when it's needed, what an ORM is actually simplifying for them, and just as importantly, when reaching for an ORM makes sense and when writing raw SQL directly is the smarter call.&lt;/p&gt;

&lt;p&gt;Once that clarity sets in, choosing between these tools stops feeling like a guessing game. You start making the call based on what the project actually needs, not on whatever's trending that particular month.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>prisma</category>
      <category>drizzle</category>
      <category>database</category>
    </item>
    <item>
      <title>Is Supabase Actually a Database?</title>
      <dc:creator>Mr. S Gupta</dc:creator>
      <pubDate>Thu, 20 Aug 2026 14:44:18 +0000</pubDate>
      <link>https://dev.to/surajsrggupta/is-supabase-actually-a-database-5bi8</link>
      <guid>https://dev.to/surajsrggupta/is-supabase-actually-a-database-5bi8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Is Supabase Actually a Database? Sorting Out Supabase, Firebase, Appwrite, PocketBase, and Neon&lt;/strong&gt;&lt;br&gt;
For a good chunk of my early days learning backend stuff, I genuinely believed Supabase, Firebase, and Appwrite were databases. Just straight up databases, no different from PostgreSQL or MongoDB in my head.&lt;/p&gt;

&lt;p&gt;And honestly, the way tutorials talk about them didn't help at all. One video would tell you to just use Supabase and move on. The next would swear by Firebase instead. Then someone else would come along and say skip all of that, just learn PostgreSQL properly.&lt;/p&gt;

&lt;p&gt;At some point I genuinely lost track of what I was even picking between. Was I choosing a database? A backend? Some kind of hosting service? Honestly I couldn't tell you at the time.&lt;/p&gt;

&lt;p&gt;If that confusion sounds familiar, this should clear most of it up.&lt;/p&gt;
&lt;h2&gt;
  
  
  The one thing you need to get straight first
&lt;/h2&gt;

&lt;p&gt;A database and a backend platform are not the same thing, and mixing them up is where basically all the confusion starts.&lt;/p&gt;

&lt;p&gt;Think about building a house for a second. You need bricks, sure, but you also need wiring, plumbing, security systems, furniture, all of it. The bricks are just one piece of a much bigger picture.&lt;/p&gt;

&lt;p&gt;A database works the same way inside your backend. It's just one piece of the whole thing. Modern platforms tend to bundle a bunch of backend services together under one roof, and that's exactly why people end up mistaking the whole platform for just the database part.&lt;/p&gt;
&lt;h2&gt;
  
  
  So what does a database actually do
&lt;/h2&gt;

&lt;p&gt;A database really only has one job, and it's not glamorous. It stores your data. That's the entire job description.&lt;/p&gt;

&lt;p&gt;Take PostgreSQL for example. It stores things in tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Rahul&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:rahul@example.com"&gt;rahul@example.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Aman&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:aman@example.com"&gt;aman@example.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It genuinely doesn't care what you're building on top of it, whether that's an online store, a social app, or some banking system. Its only concern is storing and pulling back data when asked.&lt;/p&gt;
&lt;h2&gt;
  
  
  Okay, so what is Supabase then
&lt;/h2&gt;

&lt;p&gt;Here's the thing people keep getting wrong. Supabase is not a database.&lt;/p&gt;

&lt;p&gt;Supabase is a backend platform, and one of the things it happens to include is a PostgreSQL database.&lt;/p&gt;

&lt;p&gt;Picture it more like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Supabase

├── PostgreSQL Database
├── Authentication
├── File Storage
├── Realtime Engine
├── Edge Functions
├── Dashboard
└── APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See how the database is just one item in that list, not the whole thing. Supabase basically hands you a bunch of backend pieces that are already wired together, so instead of setting up each one separately, you get a working backend pretty much out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if you skip Supabase entirely
&lt;/h2&gt;

&lt;p&gt;Nothing's actually stopping you from building this exact same setup yourself, piece by piece.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Express

├── PostgreSQL
├── JWT Authentication
├── AWS S3 Storage
├── Socket.IO
├── REST API
└── Docker Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get total control doing it this way, no argument there. But you're also signing up for a lot more setup work, and honestly a lot more things that can go wrong along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  And Firebase
&lt;/h2&gt;

&lt;p&gt;Firebase is also a backend platform, but it's built around a different core. Instead of PostgreSQL, its main database is Firestore, which is a document database, meaning your data lives as documents rather than rows in a table.&lt;/p&gt;

&lt;p&gt;Firebase brings along its own bundle of services too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Firebase

├── Firestore
├── Authentication
├── Cloud Functions
├── Cloud Storage
├── Analytics
├── Hosting
└── Push Notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever built an Android app, you've probably had Firebase suggested to you constantly, and that's mostly because Google has baked it so deeply into its mobile ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is Firebase actually better than Supabase
&lt;/h2&gt;

&lt;p&gt;There's no clean answer here, and honestly anyone who gives you one confidently is probably oversimplifying. They're just solving different problems.&lt;/p&gt;

&lt;p&gt;Supabase makes a lot of sense if you're comfortable with PostgreSQL and SQL in general. Firebase makes more sense if your app genuinely benefits from Firestore's structure or from being deep in Google's ecosystem. Which one wins really comes down to what you're building, not which platform is objectively superior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does Appwrite fit in
&lt;/h2&gt;

&lt;p&gt;Appwrite is chasing a pretty similar goal to Supabase. It gives you authentication, a database, file storage, functions, messaging, the usual suite.&lt;/p&gt;

&lt;p&gt;What sets it apart is that it's really built with self hosting in mind from the start. If the idea of running your own backend infrastructure instead of leaning on someone else's managed cloud appeals to you, Appwrite is worth a proper look.&lt;/p&gt;

&lt;h2&gt;
  
  
  And PocketBase
&lt;/h2&gt;

&lt;p&gt;PocketBase is a much smaller, lighter animal compared to Supabase or Firebase. Under the hood it's running on SQLite, which is itself a relational SQL database, and it pairs that with the handy stuff like auth and file storage.&lt;/p&gt;

&lt;p&gt;Its whole setup is refreshingly simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PocketBase

├── SQLite
├── Authentication
├── File Storage
└── Dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's so lightweight, PocketBase has become a favorite for quick prototypes, demos, and smaller side projects. It's just not usually what people reach for when they're building something that needs to scale up seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then what exactly is Neon
&lt;/h2&gt;

&lt;p&gt;Compared to everything above, Neon is honestly refreshingly simple to explain. Neon is basically managed PostgreSQL. That's genuinely it, nothing more hidden underneath.&lt;/p&gt;

&lt;p&gt;So if someone tells you they're using Neon, all that really means is their PostgreSQL database is being hosted and managed by Neon. There's no built in auth system, no storage service, no bundled backend platform sitting alongside it. Just the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application

↓

Neon

↓

PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly why a lot of developers pair Neon with something else on top, like Prisma, Drizzle, Clerk, Better Auth, or their own setup on AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying them all out side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Extra Backend Services&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase&lt;/td&gt;
&lt;td&gt;Firestore&lt;/td&gt;
&lt;td&gt;NoSQL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Appwrite&lt;/td&gt;
&lt;td&gt;SQL based database support&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PocketBase&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neon&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look closely and the pattern is obvious. Neon is the only one that's really just about hosting the database itself. Everyone else on that list gives you a whole backend ecosystem wrapped around one.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one should a beginner actually go with
&lt;/h2&gt;

&lt;p&gt;Don't pick based on whatever's trending on YouTube this month, that's honestly how a lot of people end up confused in the first place. Pick based on what you're actually trying to learn.&lt;/p&gt;

&lt;p&gt;If you want to genuinely understand relational databases, plain PostgreSQL is a solid starting point. If you want authentication, storage, and a dashboard without burning days configuring everything yourself, Supabase is a great pick. If you're building mobile apps and want tight integration with Google's ecosystem, Firebase is the logical choice. If you're just messing around with side projects or prototypes, PocketBase is genuinely convenient. And if owning your own infrastructure matters to you, Appwrite is worth exploring properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake almost everyone makes early on
&lt;/h2&gt;

&lt;p&gt;A lot of people end up comparing PostgreSQL directly with Supabase, and honestly that comparison just doesn't hold up. It's a bit like comparing an engine to an entire car.&lt;/p&gt;

&lt;p&gt;The comparisons that actually make sense look more like this. PostgreSQL against Firestore. Or Supabase against Firebase. One of those is a database versus database comparison, the other is a platform versus platform comparison. Once that distinction clicks, a huge chunk of the confusion just disappears on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Whenever some new backend tool crosses your radar, there's really just one question worth asking before anything else. Is this actually a database, or is it a platform built around one?&lt;/p&gt;

&lt;p&gt;That single question quietly untangles most backend architecture confusion. Once you know exactly what each piece of the puzzle is actually responsible for, choosing between tools stops feeling like a random guessing game and starts feeling a lot more deliberate.&lt;/p&gt;

&lt;p&gt;Next time, I want to go one level up from the database itself and tackle another question beginners run into constantly, what exactly is an ORM, and why do people reach for tools like Prisma or Drizzle instead of just writing raw SQL themselves?&lt;/p&gt;

</description>
      <category>sql</category>
      <category>supabase</category>
      <category>appwritehack</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
