Chapter 26: Upgrading from v2 to v3
1. Overview
Tina4 v3 is a ground-up rewrite. The API surface is similar. The internals are completely different.
The three biggest changes:
- Zero npm dependencies. v3 uses only Node built-in modules.
node:sqlitereplacesbetter-sqlite3.node:testreplaces Jest. Nonode-gyp. No native binaries.npm installfinishes in seconds. - TypeScript-first. All source files are
.ts. The framework compiles and runs them directly. No separate build step. - Naming conventions. Properties use
camelCase. Classes usePascalCase. The ORM converts betweencamelCaseTypeScript properties andsnake_casedatabase columns automatically.
If your v2 project is small, a fresh tina4 init and copying your logic across is faster than an in-place upgrade. If your project is large, this chapter walks through every change systematically.
2. Package and Installation
v2
npm install tina4-nodejs
# Required Node 18+v3
npm install tina4-nodejs
# Requires Node 22+Node 22 is required because v3 uses node:sqlite, which shipped as a built-in module starting in Node 22. Check your version:
node --version
# Must be v22.0.0 or higherIf you are on an older Node, upgrade first. Everything else depends on it.
3. Project Structure Changes
v2 Structure
src/
├── app.ts
├── routes/
│ └── *.ts
├── models/
│ └── *.ts
└── views/
└── *.htmlv3 Structure
src/
├── routes/
│ └── *.ts
├── orm/
│ └── *.ts
└── templates/
└── *.htmlKey differences:
models/is noworm/. The directory name reflects what it does.views/is nowtemplates/. Frond loads fromsrc/templates/.- No
app.tsentry point. v3 auto-discovers all.tsfiles insrc/routes/andsrc/orm/recursively. Drop a file in. It loads. - Migrations stay in
migrations/at the project root. No change there.
What to Do
- Rename
src/models/tosrc/orm/ - Rename
src/views/tosrc/templates/ - Move your route files into
src/routes/if they are not already there - Delete
app.ts-- v3 does not need it
4. Routing Changes
v2 Routing
// v2
import { Tina4 } from "tina4-nodejs";
Tina4.get("/hello", (req, res) => {
res.json({ message: "Hello" });
});v3 Routing
// v3
import { Router } from "tina4-nodejs";
Router.get("/hello", async (req, res) => {
return res.json({ message: "Hello" });
});What changed:
Tina4.get()becomesRouter.get(). Same forpost(),put(),patch(),delete().- Handlers must be
asyncand mustreturnthe response. Forgettingreturnproduces empty responses. - Auth defaults:
POST,PUT,PATCH, andDELETEroutes are secured by default.GETroutes are public. Use@noauthin a JSDoc comment to make a write route public. Use@securedor.secure()to protect a GET route. - Middleware is referenced by function name as a string, not passed inline.
v2 Middleware
// v2
Tina4.get("/admin", checkAuth, (req, res) => {
res.json({ page: "admin" });
});v3 Middleware
// v3
Router.get("/admin", async (req, res) => {
return res.json({ page: "admin" });
}, "checkAuth");The middleware function itself is defined as a named function in any auto-loaded file. Tina4 resolves it by name at runtime.
5. Database Changes
Connection Strings
v3 uses the same DATABASE_URL environment variable, but the format is standardised:
# SQLite (default if DATABASE_URL is not set)
DATABASE_URL=sqlite:///data/app.db
# PostgreSQL
DATABASE_URL=postgres://localhost:5432/myapp
# MySQL
DATABASE_URL=mysql://localhost:3306/myapp
# Firebird
DATABASE_URL=firebird://localhost:3050/path/to/database.fdbSQLite: node:sqlite
v2 used better-sqlite3 (a native C++ addon). v3 uses node:sqlite, which is built into Node 22. No compilation. No platform-specific binaries. If Node runs, the database works.
Database API
// v2
const db = Tina4.getDatabase();
const rows = db.query("SELECT * FROM products");
// v3
import { Database } from "tina4-nodejs/orm";
const db = Database.getConnection();
const rows = await db.fetch("SELECT * FROM products");All v3 database methods are async. fetch() returns a DatabaseResult object that is iterable and carries metadata (.records, .columns, .count).
Firebird: Lowercase Column Names
Firebird stores column names in uppercase by default. v3 normalises them to lowercase in query results. If your v2 code accesses row.FIRST_NAME, change it to row.first_name.
6. ORM Changes
Class Definition
// v2
import { BaseModel } from "tina4-nodejs/orm";
export class Product extends BaseModel {
tableName = "products";
primaryKey = "id";
id: number;
name: string;
price: number;
}
// v3
import { BaseModel } from "tina4-nodejs/orm";
export class Product extends BaseModel {
static tableName = "products";
static primaryKey = "id";
id!: number;
name!: string;
price: number = 0.00;
inStock: boolean = true;
createdAt!: string;
}Changes:
tableNameandprimaryKeyare nowstaticproperties.- Properties use
camelCase. The ORM maps them tosnake_casecolumns automatically:inStockmaps toin_stock,createdAtmaps tocreated_at. - Use
!:for required fields and= valuefor defaults.
Auto-Mapping with autoMap
v3 introduces static autoMap = true. When enabled, the ORM auto-generates fieldMapping entries from your camelCase property names to snake_case database column names. You do not need to write them by hand.
import { BaseModel } from "tina4-nodejs/orm";
export class Customer extends BaseModel {
static tableName = "customers";
static primaryKey = "id";
static autoMap = true;
id!: number;
firstName!: string; // auto-maps to "first_name"
lastName!: string; // auto-maps to "last_name"
emailAddress!: string; // auto-maps to "email_address"
createdAt!: string; // auto-maps to "created_at"
}No fieldMapping needed. The ORM inspects the property names, converts them with camelToSnake(), and builds the mapping at runtime.
Explicit fieldMapping Takes Precedence
If a column does not follow the snake_case convention (legacy databases, third-party schemas), add an explicit fieldMapping entry. It overrides the auto-generated mapping for that field:
export class User extends BaseModel {
static tableName = "user_accounts";
static primaryKey = "id";
static autoMap = true;
static fieldMapping = {
firstName: "fname", // overrides auto-map ("first_name" → "fname")
lastName: "lname", // overrides auto-map ("last_name" → "lname")
};
id!: number;
firstName!: string; // maps to "fname" (explicit)
lastName!: string; // maps to "lname" (explicit)
emailAddress!: string; // maps to "email_address" (auto-mapped)
}Explicit entries win. Auto-mapped entries fill in the rest.
Utility Functions
v3 exports snakeToCamel() and camelToSnake() for use in your own code:
import { snakeToCamel, camelToSnake } from "tina4-nodejs/orm";
snakeToCamel("first_name"); // "firstName"
snakeToCamel("created_at"); // "createdAt"
camelToSnake("firstName"); // "first_name"
camelToSnake("createdAt"); // "created_at"Output Methods
toDict()returnssnake_casekeys (matching database columns). Use for API responses.toObject()returnscamelCasekeys (matching TypeScript properties). Use internally.
7. Template Engine Changes
Templates still use Frond with the same Twig-compatible syntax. Two things changed.
Cached Instances
v3 caches compiled Frond template instances. The first render compiles the template. Subsequent renders reuse the compiled version. No code change needed on your side -- it happens automatically. Expect faster response times on template-heavy pages.
Method Calls on Object Values
v3 adds the ability to call methods on object values inside templates, with arguments:
<!-- v2: not possible -->
<!-- v3: call methods with arguments -->
<p>{{ user.t("greeting_key") }}</p>
<p>{{ product.formatPrice("USD") }}</p>
<p>{{ order.statusLabel() }}</p>The object passed to the template must have the method defined. Frond calls it and outputs the return value. Arguments are passed as literals (strings, numbers, booleans).
8. Migration Tracking Table
v2 used a migration tracking table (sometimes named tina4_migrations or a similar variant). v3 uses a table named tina4_migration with a specific schema:
| Column | Purpose |
|---|---|
id | Auto-incrementing primary key |
description | Migration filename |
content | Full SQL text (for audit) |
passed | Whether it ran successfully |
batch | Batch number for rollback grouping |
run_at | Timestamp |
When v3 starts and detects a v2 tracking table, it auto-upgrades the table structure. Your existing migration history is preserved. Already-applied migrations will not run again.
No manual intervention required. Run tina4 migrate and v3 handles the rest.
9. New Features in v3
Things you get by upgrading that did not exist in v2:
- File-based routing. The file path becomes the URL.
src/routes/api/products/get.tshandlesGET /api/products. - Typed path parameters.
{id:int},{price:float},{slug:alpha},{filepath:path}. - Route chaining.
.secure()and.cache()on any route. - Auto-CRUD.
static autoCrud = trueon a model generates full REST endpoints. - Soft delete.
static softDelete = truewithdeletedAtfield. - Eager loading. Pass relationship names to
select()to avoid N+1 queries. - Connection pooling. Pass a pool size to
Database.create(). - DatabaseResult. Iterable results with
.toPaginate(),.toCsv(),.toJson(),.columnInfo(). - Query builder. Fluent API for building queries without raw SQL.
- Batch rollback.
migrate:rollbackundoes an entire batch, not just one migration. - GraphQL support. Built-in GraphQL endpoint generation from ORM models.
- WebSocket support. Native WebSocket server and client.
- Queue system. Background job processing.
10. Step-by-Step Migration Checklist
Follow this order. Each step builds on the previous one.
1. Upgrade Node
node --version
# If below v22, upgrade
nvm install 22
nvm use 222. Update package.json
npm install tina4-nodejs@latestRemove any Tina4-related dependencies that are no longer needed (better-sqlite3, pg, etc. -- v3 bundles its own drivers or uses Node built-ins).
3. Rename Directories
mv src/models src/orm
mv src/views src/templates4. Update Imports
Find and replace across your codebase:
| v2 | v3 |
|---|---|
import { Tina4 } from "tina4-nodejs" | import { Router } from "tina4-nodejs" and import { Database } from "tina4-nodejs/orm" |
Tina4.get(...) | Router.get(...) |
Tina4.post(...) | Router.post(...) |
Tina4.getDatabase() | Database.getConnection() |
db.query(...) | await db.fetch(...) |
5. Make Route Handlers Async and Return Responses
// Before
Router.get("/hello", (req, res) => {
res.json({ message: "Hello" });
});
// After
Router.get("/hello", async (req, res) => {
return res.json({ message: "Hello" });
});6. Update ORM Models
- Make
tableNameandprimaryKeystatic - Rename properties to camelCase
- Add
static autoMap = trueif you want automatic field mapping - Add explicit
fieldMappingonly for columns that do not follow snake_case
7. Update Database Calls
- Add
awaitto all database operations - Replace
db.query()withdb.fetch(),db.fetchOne(), ordb.execute() - Update Firebird column references from uppercase to lowercase
8. Update Template References
- Move templates from
src/views/tosrc/templates/ - Update any
res.html()calls if the path changed - Template syntax (Frond/Twig) is unchanged -- no edits needed there
9. Run Migrations
tina4 migratev3 auto-upgrades the tracking table. Existing migrations are preserved.
10. Test
tina4 serveHit every endpoint. Check every page. The framework runs node:test for unit tests:
tina4 test11. Remove Dead Code
- Delete
app.tsif it exists - Remove any manual server startup code -- v3 handles it
- Remove
node_modulesand reinstall cleanly:
rm -rf node_modules package-lock.json
npm installGotchas
1. Node Version Too Low
Problem: Error: Cannot find module 'node:sqlite'
Cause: You are running Node 20 or earlier.
Fix: Upgrade to Node 22+.
2. Missing Return in Route Handlers
Problem: Routes return empty responses or 500 errors.
Cause: v3 requires return res.json(...). v2 was more forgiving.
Fix: Add return before every res.json(), res.html(), and res.status() call.
3. Sync Database Calls
Problem: Database queries return Promise {<pending>}.
Cause: v3 database methods are all async. v2 had some sync methods.
Fix: Add await to every db.fetch(), db.execute(), db.fetchOne(), and ORM method.
4. Uppercase Firebird Columns
Problem: row.FIRST_NAME is undefined.
Cause: v3 normalises Firebird column names to lowercase.
Fix: Change to row.first_name.
5. Middleware Passed as Function Reference
Problem: Inline middleware functions cause errors.
Cause: v3 resolves middleware by function name (string), not by reference.
Fix: Define middleware as a named function and pass the name: "checkAuth", not checkAuth.