Skip to content
Tikab's Toolkit

Coming from Django

Same needs, new homes. This page shows the mapping three ways: the table, the request flow, and the code.

The translation table

DjangoHereWhere
ORM (models)Drizzle ORMDatabase
makemigrations / migratedrizzle-kit generate / migratepackages/db/drizzle/
Views / DRF / NinjaServer functionsServer functions
Forms / serializersZod (inputValidator)inside each server function
django.contrib.authBetter AuthAuth
login_requiredrequireSession()@repo/auth/session
is_staff / is_superuserisSiteAdmin + requireSiteAdmin()example/src/lib/access.ts
guardian / rules (object permissions)typescript-rules + capabilitiesexample/src/lib/permissions/
The admin (changelist, actions)@repo/admin-ui + /adminAdmin UI
django-import-export (preview → confirm)@repo/import-exportImport/export
Celery + beatHatchetBackground jobs
send_mailsendEmail()Email
django-storagesputObject / getObjectStorage
constance (runtime settings)getConfig / setConfigRuntime config
Signals → audit trailexplicit recordAudit()Audit
Templates (the T in MTV)React with SSRexample/src/routes/
gettext / django.po i18nParaglide JS — typed message functionsexample/messages/sv.json + en.json
settings.py.env + validated envEnvironment

Three things disappear rather than map:

  • CORS — frontend and backend are the same origin, the same server.
  • CSRF token plumbing — SameSite cookies + JSON payloads.
  • DRF routers — file-based routing plus typed function calls.

The request flow

How Django serves our classic React frontend:

Loading diagram...

The same operation here:

Loading diagram...

That last note is the biggest day-to-day quality difference: one contract, one place. In the Django setup we maintain the serializer and the TS types separately; here the function signature is the contract.

The code: validation and types in one

In Django you write a serializer and hope the frontend types match. Here the Zod schema defines both the validation and the type — hover Input:

import {  } from "zod";
 
const  = .({
  : .(),
  : .().(1).(120),
  : .().(60).(),
});
 
type  = .<typeof >;

And mistakes surface at compile time, not in production — count is not in the schema:

import {  } from "zod";
 
const  = .({ : .() });
 
.({ : "K-01" });
type  = .<typeof >;
 
const :  = { : "K-01", : 3 };
Object literal may only specify known properties, and 'count' does not exist in type '{ name: string; }'.

The model: same idea, new syntax

Django's models.Model becomes a pgTable. This is the real user table from the Toolkit's foundation schema — included live from the source, not a copy:

export const user = pgTable("user", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  emailVerified: boolean("email_verified").notNull().default(false),
  image: text("image"),
  isSiteAdmin: boolean("is_site_admin").notNull().default(false),
  deactivatedAt: timestamp("deactivated_at"),
  // Free-form per-user key/value store. Used by `user_info` tasks to persist
  // collected field values (keyed by `task:<id>`); other future profile data
  // can live here too.
  metadata: jsonb("metadata"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

Things to notice:

  • No class magic. The table is a value; the row type is derived (typeof user.$inferSelect).
  • Migrations are generated from code, like makemigrations, but the diff is plain SQL in packages/db/drizzle/ — readable in code review.
  • is_active is deactivatedAt — when set, the user is locked out on the next request (see Auth).