Skip to content

Migration/bun rescript#4

Draft
GhCristea wants to merge 82 commits intomainfrom
migration/bun-rescript
Draft

Migration/bun rescript#4
GhCristea wants to merge 82 commits intomainfrom
migration/bun-rescript

Conversation

@GhCristea
Copy link
Owner

No description provided.

GhCristea added 30 commits March 1, 2026 05:07
- Configure ReScript to emit ESM for Bun compatibility
- Set in-source compilation to keep .res and .res.js files alongside each other
- Add rescript-schema as build dependency for runtime validation
- Set suffix to .res.js to distinguish compiled ReScript files

This enables the ReScript compiler to output Bun-compatible JavaScript while
maintaining a clear project structure.
- Remove: better-sqlite3 (Bun.sql built-in), tsx (Bun native), TypeScript
- Add: rescript (compiler), rescript-schema (validation)
- Update scripts: bun --watch for dev, rescript build for compilation
- Keep: Express (works on Bun), cors, @types/express for FFI bindings

This creates a minimal, focused dependency tree optimized for ReScript+Bun.
No runtime overhead from TypeScript compilation or alternative SQLite drivers.
- Bind bun:sqlite Database and prepared statement types
- Expose: open, prepare, all, get, run for query execution
- Include transaction support: begin, commit, rollback
- Use abstract types (database, statement) to prevent accidental misuse
- Parameters passed as spread (...array<'b>) for flexibility
- run() returns {changes, lastInsertRowid} for mutation feedback

These bindings replace better-sqlite3 and expose Bun's built-in high-performance
SQLite interface. The abstract types ensure type safety at the Bun boundary.
Instead of a generic QueryBuilder with type parameters, implement explicit,
straightforward query functions:

- selectAll, findById, findByCategory, findByName, findWithPagination
- insertItem, updateItem, deleteById
- Utility: exists, count

Each function:
- Has a concrete signature (no type params)
- Uses Result<T, string> for error handling
- Handles parameters explicitly (no dynamic binding)
- Returns typed itemRow (not generic 'a)

This approach:
✓ Eliminates abstraction complexity
✓ Makes all query logic visible and debuggable
✓ Easier to review and refactor
✓ Can scale to CategoryQueryBuilder, OrderQueryBuilder, etc. if needed
✓ No phantom types or advanced generics to maintain

Boundary: All Bun.sqlite calls are here. Business logic calls these functions.
- Item record: id, name, description, categoryId, createdAt, updatedAt
- fromRow: converts QueryBuilder.itemRow to domain Item type
- createTableSQL: SQL to initialize items table with FK to categories
- createCategoryIndexSQL: index for efficient category lookups

This entity layer keeps domain types separate from database representation,
making it easy to evolve the model independently of the DB schema.
Replace Zod with rescript-schema for fully type-safe validation:

- createItemSchema: validates name (3-100 chars), optional description, categoryId
- updateItemSchema: all fields optional (partial updates)
- itemResponseSchema: response DTO matching API contract
- validateCreateItem, validateUpdateItem: parse and validate request bodies
- toResponse: convert Item entity to API response

Key difference from Zod: S.Output.t<typeof schema> is PROVEN correct by
the type checker—there's no escape hatch like z.any() or zod.coerce().
Every validation path is type-safe.
Replace AppError class with exhaustive error variants:
- NotFound, ValidationError, Conflict, Unauthorized, Forbidden, Internal
- statusCode: maps error to HTTP status
- message: converts error to user-facing string
- toResponse: creates JSON error response

Compiler benefits:
✓ Exhaustive pattern matching—impossible to miss error cases
✓ No null checks or type guards needed
✓ Clear error propagation through Result<T, AppError>
✓ Every possible failure mode is explicit in the type system

This eliminates entire classes of runtime errors that plagued the class-based approach.
Business logic layer:
- Query ops: getAll, getOne, getByCategory, search
- Mutations: create, update, delete
- All return Result<T, AppError> — no exceptions escape
- DB dependency injected via setDatabase()
- Exhaustive error handling on every QueryBuilder call

Key patterns:
✓ getDb() validates DB is initialized before any query
✓ Result.flatMap chains operations with error propagation
✓ Impossible to accidentally return null or undefined
✓ Each error case is explicit and catchable
✓ Service is pure — only side effect is DB I/O

Next layer (HTTP) will unwrap Results and convert to responses.
- initialize: opens database, creates schema, injects DB into services
- destroy: closes connection and cleans up
- getInstance, getDatabase: accessors for DB reference

Flow on startup:
1. AppDataSource.initialize() -> opens data.db
2. Runs PRAGMA foreign_keys = ON
3. Executes Item.createTableSQL and indexes
4. Calls ItemService.setDatabase(db) to inject DB
5. Sets isInitialized = true

On shutdown:
1. AppDataSource.destroy() -> db->close()
2. Clears dataSource reference

This pattern ensures:
✓ Single DB connection shared across services
✓ Schema is created once on startup
✓ Clean shutdown on process termination
✓ Easy to add more entities (Category, Order, etc.)
Bind only what we use:
- Application creation, routing (GET/POST/PUT/PATCH/DELETE)
- Request: method, path, params, query, body
- Response: status, json, send, setHeader
- Middleware: cors, json parser
- Error handler type for error middleware

This is a minimal surface that covers our current needs.
If we need more (e.g., sessions, auth middleware), we add bindings incrementally.

Alternative: Switch to Bun.serve later for better performance.
For now, Express is lower-risk and enables us to test business logic first.
Handlers for all CRUD operations:
- list: GET /rest/items (all items)
- get: GET /rest/items/:id (single item)
- create: POST /rest/items (new item with validation)
- update: PUT /rest/items/:id (partial or full update)
- delete: DELETE /rest/items/:id (remove item)

Each handler:
✓ Parses request (params, body)
✓ Validates input via ItemDto schemas
✓ Calls ItemService (business logic)
✓ Handles Result<T, AppError>
✓ Maps error to HTTP status code
✓ Returns JSON response

Error handling is exhaustive:
- Request parsing errors -> 400
- Validation errors -> 400
- Not found -> 404
- Internal errors -> 500

No exceptions escape to the error middleware.
Express router with CRUD endpoints:
- GET /  -> list all items
- GET /:id -> fetch single item
- POST / -> create item
- PUT /:id -> update item
- DELETE /:id -> delete item

All routes bound to ItemsController handlers.
Router is mounted at /rest/items in the main app.

This is the final layer before the entry point.
- Initialize Express app with middleware (cors, json parser)
- Await AppDataSource.initialize() to set up database and schema
- Mount ItemsRouter at /rest/items
- Install error handler middleware
- Listen on port 3001
- Handle graceful shutdown (Ctrl+C, SIGTERM)

Flow on startup:
1. Express app created
2. Middleware registered
3. Database initialized (schema created, services injected)
4. Routes mounted
5. Error middleware installed
6. Server listening

Flow on shutdown:
1. SIGINT/SIGTERM caught
2. AppDataSource.destroy() closes DB connection
3. Process exits cleanly

Application is now fully functional with ReScript + Bun + Express.
Documents the complete TypeScript → Bun + ReScript migration:

- Architectural changes (concrete QueryBuilder, type-safe errors, Result<T,E>)
- Project structure and layer organization
- Commit sequence (13 atomic, bottom-up commits)
- Testing instructions
- Design decisions and trade-offs
- Potential gotchas and mitigations

This serves as both documentation and a review guide for understanding
each commit's purpose and how layers connect.
- Ignore ReScript compiled output (*.res.js, _build/)
- Ignore SQLite database files (*.db, *.sqlite, *.sqlite3)
- Ignore Bun lock file (bun.lockb)
- Keep existing Node/IDE ignores

Prevents compiled ReScript and database snapshots from being committed.
Provides:
- Quick visual overview of all 14 commits
- Architectural comparison table (TypeScript vs ReScript)
- Full end-to-end request flow with code examples
- Architecture diagram showing layer stack
- How to review and test
- Key ReScript patterns used
- Performance characteristics
- Known limitations (all intentional)
- Pre-merge checklist

Serves as the entry point for understanding the complete redesign.
Remove:
- express (replace with Bun.serve)
- cors (use Response headers directly)
- @types/express, @types/cors, @types/node (no longer needed)
- eslint and TypeScript tooling (ReScript provides safety)

Keep:
- rescript (compiler)
- rescript-schema (validation)

Dependencies:
✓ Bun.sqlite - built-in
✓ Bun.serve - built-in
✓ JSON parsing - built-in
✓ URL routing - hand-rolled in ReScript

Result: Zero external dependencies for runtime or HTTP handling.
Only rescript-schema for validation (no replacements available).
Replace Express entirely with Bun's native HTTP server:

- request: Fetch API Request type
- response: Fetch API Response type
- fetchHandler: async function that takes request -> response
- json: Create JSON response with CORS headers
- empty: Create 204 No Content response
- corsPreflightResponse: Handle OPTIONS preflight

Benefits:
✓ Built-in to Bun (no npm install)
✓ Fetch API standard (portable code)
✓ CORS headers set directly (no middleware)
✓ Zero external dependencies
✓ Significantly faster than Express

Design:
- Router logic (URL parsing) is in ReScript
- This module only handles HTTP primitives
- Response construction includes CORS headers by default
Route matching:
- parseRoute: match URL patterns (/rest/items/:id) against paths
- Extract parameters from URLs
- Support GET, POST, PUT, DELETE, PATCH, OPTIONS

Router:
- register: add route + handler
- dispatch: find matching route and execute handler
- Handles CORS preflight (OPTIONS) automatically

Convenience:
- router.get/post/put/delete/patch helpers

Benefits:
✓ 100% ReScript (no external router library)
✓ Explicit route matching logic (easy to debug)
✓ CORS built-in (no middleware needed)
✓ Lightweight (only code we need)
✓ Type-safe parameter extraction

Future: Add pattern matching improvements, catch-all routes, etc.
All handlers now use Fetch API directly:
- request is Fetch API Request
- response is Fetch API Response
- No Express types or middleware

Helpers:
- parseJsonBody: async parse with error handling
- getIdParam: extract and validate ID from URL

Handlers:
- list, get, create, update, delete
- All return json() or empty() responses with CORS headers
- All handle Result<T, E> exhaustively

Benefits:
✓ Pure standard Fetch API
✓ No Express-specific code
✓ No type adapter layer needed
✓ Easier to port to other Bun patterns later

Response building uses BunServer.json/empty which include CORS headers.
Replaced by Router.res (pure ReScript) and Bun.serve (native HTTP).
Old module no longer needed.
Replaced by BunServer.res (Bun.serve native bindings).
Express dependency completely removed from package.json.
Application flow:
1. Initialize database (AppDataSource)
2. Create router and register routes
3. Define fetch handler (request -> response)
4. Start Bun.serve with fetch handler
5. Handle graceful shutdown

What's gone:
- Express app initialization
- Middleware setup (cors, json)
- app.listen(...)

What's new:
- Router.create() and Router.get/post/etc
- handleRequest: async fetch handler
- BunServer.serve(options) with fetch function

URL routing entirely in ReScript (Router.dispatch).
No external HTTP framework. Bun.serve handles all network I/O.

Dependencies:
✓ Bun (built-in)
✓ ReScript (compiler)
✓ rescript-schema (validation)
✓ NOTHING ELSE

This is the simplest, fastest HTTP server we can have in Bun.
Removed dependencies:
- express
- express-async-errors
- cors
- Any other external HTTP middleware

Reason: Replaced by Bun.serve native API and ReScript routing.

Final dependencies:
- bun (runtime + HTTP server)
- rescript (compiler)
- rescript-schema (validation only)

DevDependencies:
- @types/bun (TypeScript types for IDE/tooling)

Scripts:
- dev: bun run src/index.res (direct execution)
- build: rescript build -to-js (compile to JavaScript)
- start: bun dist/index.js (run compiled JS)
- db:* scripts via bun (database utilities)

This is the absolute minimum to run a REST API.
No framework bloat. No middleware boilerplate.
Everything in ReScript, optimized for Bun.
Defines:
- Item.t: full entity with id and timestamps
- Item.createInput: DTO for POST /items
- Item.updateInput: DTO for PATCH /items/:id

These replace the TypeScript DTOs (item.dto.ts).
Used by:
- ItemsController.res (handlers)
- ItemService.res (business logic)
- Schemas.res (validation)

Type-safe across the entire API stack.
Defines:
- itemCreateSchema: validates POST /items body
- itemUpdateSchema: validates PATCH /items/:id body
- parseCreateInput: parse + validate JSON
- parseUpdateInput: parse + validate JSON
- formatErrors: convert errors to string for responses

This is the single source of truth for all input validation.
Replaces scattered TypeScript validation decorators.

Type-safe parsing: Result<input, errors>
Zero runtime overhead: compiles to optimized JS.
Defines:
- NotFound: 404
- ValidationError: 400 (with error list)
- Conflict: 409
- Internal: 500

Functions:
- toStatus: error → HTTP status
- toMessage: error → string message
- toResponse: error → HTTP response

Compiles to minimal JavaScript.
No exceptions or try/catch in business logic.
Errors flow as Result types through the system.
Defines:
- deps type: interface for data access
- mockList/get/create/update/delete: in-memory defaults
- default: service instance

Dependency Injection allows:
- Testing: inject mock deps
- Flexibility: swap database implementations
- Clarity: explicit dependencies

Will be populated by AppDataSource.ts later.
Controllers receive deps and call service methods.
Handlers (type: request -> promise<response>):
- list: GET /items
- get: GET /items/:id
- create: POST /items
- update: PATCH /items/:id
- delete: DELETE /items/:id

Inline parsing:
- readBody: extract request body
- getIdParam: parse route param to int
- Schemas.parse*: validate JSON

Error handling:
- All errors flow through Result types
- AppError.toResponse: convert to HTTP response

Dependency Injection:
- Controllers use ItemService.default (injected deps)
- Services are polymorphic (can be mocked)

No exceptions. Pure functions. Type-safe.
Functions:
- extractParams: parse /items/:id from path
- matchRoute: match METHOD + PATH to handler
- dispatch: main entry point for request routing

Supports:
- GET /items
- GET /items/:id
- POST /items
- PATCH /items/:id
- DELETE /items/:id

Pattern matching:
- Static paths: exact match
- Dynamic params: :id extracted to params dict
- Returns Option (Some route, None if no match)

Type-safe handler dispatch.
Zero regex. Pure string matching.
GhCristea added 30 commits March 1, 2026 06:37
…cture

- bindings/Bun.res: minimal FFI — serve, sqlite, URL.pathname, req.json()
- core/AppError.res: error variants + toResponse (adds BadRequest)
- core/Result.res: ROP combinators — map, flatMap, fromOption
- core/Item.res: type t + createInput + updateInput + toJson (single serializer)
- core/Schemas.res: modern S.schema API, schema IS the type, single file
- db/Db.res: open bun:sqlite + run migrations
- db/ItemRepo.res: findAll, findById, insert, patch, delete — db threaded as param
- api/ItemService.res: DI record wired to ItemRepo, ROP pipelines
- api/ItemsController.res: uniform handler type, -> pipelines, zero duplication
- api/Router.res: URL.pathname fix, pattern match dispatch
- api/Server.res: Bun.serve + graceful shutdown
- Index.res: db init -> server start
- db/Schema_.res: DSL-style table definitions (inspired by rescript-sql)
- db/Schema.res: typed row/insert/update shapes derived from Schema_
- db/Db.res: updated DDL — categories + items with FK
- db/CategoryRepo.res: findByItemId
- db/ItemRepo.res: findAll(search,limit), insert, insertMany, replace, replaceMany, deleteMany
- core/Category.res: type t + toJson
- core/Item.res: add categoryId field
- core/Schemas.res: replaceInput, bulkReplaceInput, bulkDeleteInput, S.union poly body
- api/ItemService.res: full op set
- api/ServiceRegistry.res: wire CategoryRepo
- api/ItemsController.res: all 8 handlers
- api/Router.res: /rest prefix, /items/:id/categories, bulk routes
- Readme.md: accurate stack, full endpoint tables, env vars
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant