kancil-framework
v0.1.0
Published
Bun-native MVC framework untuk aplikasi web. **Zero npm framework dependencies** — tidak menggunakan Express, Hono, Fastify, atau framework JS lainnya. Dibangun di atas `Bun.serve({ routes })`, Handlebars views, dan Bun.SQL database.
Maintainers
Readme
kancil-framework
Bun-native MVC framework untuk aplikasi web. Zero npm framework dependencies — tidak menggunakan Express, Hono, Fastify, atau framework JS lainnya. Dibangun di atas Bun.serve({ routes }), Handlebars views, dan Bun.SQL database.
⚠️ Bun-only. Membutuhkan Bun 1.3.14+.
Instalasi
bun add kancil-framework handlebarsFitur opsional (PDF, QR, Barcode):
bun add pdfkit qrcode bwip-jsQuick Start
project/
server.js
.env
app/
themes/default/
layout.hbs
nav.hbs
footer.hbs
index.hbs
modules/
page/
routes.js
controllers/PageController.js
migrations/
seeds/
storage/logs/server.js
import { Application, Config, DB, Cors, Migration, Seeder } from 'kancil-framework';
const config = new Config();
DB.init(config.get('DATABASE_URL'));
await Migration.run(import.meta.dir + '/migrations');
await Seeder.run(import.meta.dir + '/seeds');
const app = new Application({ basePath: import.meta.dir + '/app' });
app.use(Cors());
await app.loadModules(import.meta.dir + '/app/modules');
await app.listen();.env
PORT=3000
DATABASE_URL=mysql://root:root@localhost:3306/myapp
APP_KEY=your-random-32-char-key-here
THEME=default
APP_ENV=developmentModule app/modules/page/routes.js
import PageController from './controllers/PageController.js';
export default app => {
app.get('/', PageController.home);
};Controller app/modules/page/controllers/PageController.js
import { Reply, View } from 'kancil-framework';
export default class PageController {
static home(req) {
return Reply.html(View.render('index', { title: 'Home' }));
}
}Template app/themes/default/index.hbs
<h1>{{title}}</h1>Project Structure
app/
themes/{theme}/
layout.hbs ← layout utama ({{body}})
{page}.hbs ← halaman per view
partials/ ← partial (nav.hbs, footer.hbs, pagination.hbs)
_system/partials/ ← partial sistem (override kalo perlu)
modules/{name}/
module.js ← { prefix: '/custom-path' } (opsional)
routes.js ← export default app => { ... }
controllers/ ← class dengan static method handlers
models/ ← extends Model { static table = '...' }
migrations/ ← { up: async db => { ... }, down: ... }
seeds/ ← { up: async db => { ... }, down: ... }
storage/
logs/ ← {date}.log
trash.json ← auto-generated oleh TrashRouting
app.get('/path', handler);
app.post('/path', csrf, handler);
app.put('/path', handler);
app.delete('/path', handler);Middleware via parameter array:
app.get('/protected', authMiddleware, handler);Prefix module otomatis berdasarkan nama folder, atau via module.js:
// app/modules/auth/module.js
export default { prefix: '/auth' };
// routes.js: app.get('/login', ...) → /auth/loginCore API
Application
| Method | Description |
|--------|-------------|
| get(path, ...handlers) | Route GET |
| post(path, ...handlers) | Route POST |
| use(middleware) | Global middleware |
| tap(fn) | Response tap — transform reply sebelum dikirim |
| loadModules(dir) | Auto-discover modules |
| listen() | Start server |
Reply
| Method | Description |
|--------|-------------|
| Reply.html(str) | HTML response |
| Reply.json(data) | JSON response |
| Reply.redirect(url) | Redirect 302 |
| Reply.text(str) | Plain text |
| Reply.pdf(buffer, filename) | PDF download |
| Reply.xlsx(data, filename) | XLSX download |
| Reply.csv(data, filename) | CSV download |
| .withFlash(key, val) | Flash message (session) |
View
View.render('template', { data }) // render + layout
View.renderPartial('name', { data }) // partial tanpa layoutFlash otomatis tersedia sebagai {{flash.success}} / {{flash.error}} di template.
Handlebars helpers: {{rupiah 100000}} → Rp 1.000.000, {{#if_eq a b}}...{{/if_eq}}.
DataTable
Server-side DataTable dengan AJAX, search, sort, pagination, bulk actions.
import { DataTable, Reply, View } from 'kancil-framework';
const options = {
columns: {
nama: { label: 'Nama', sortable: true },
harga: { label: 'Harga', sortable: true, render: (v) => 'Rp ' + v.toLocaleString('id-ID') }
},
actions: {
edit: '/produk/{id}/edit',
delete: '/produk/{id}/delete'
},
searchable: ['nama'],
orderable: { nama: 'nama' },
defaultOrder: 'id DESC',
bulkActions: true,
bulkDeleteUrl: '/produk/bulk-delete'
};
// Page handler
static async page(req) {
return Reply.html(View.render('datatable', {
...await DataTable.make(req, Produk, options)
}));
}
// AJAX data handler
static async data(req) {
return Reply.json(await DataTable.make(req, Produk, options));
}
// Bulk delete — 1 baris
static async bulkDelete(req) {
return DataTable.bulkDeleteReply(req, Model, '/produk');
}Template:
<input type="text" id="search-input" onkeyup="DataTable.search()">
<div id="table-wrapper" data-dt-url="/produk/data">{{{tableHtml}}}</div>
<div id="pagination-wrapper">{{{paginationHtml}}}</div>Trash
Otomatis mencatat soft-deleted record dari semua model ke storage/trash.json.
import { Trash } from 'kancil-framework';
Trash.list(search, page, perPage); // { data, total, pages }
await Trash.restore(id); // restore data + hapus dari trash
await Trash.remove(id); // hapus dari trash tanpa restore
Trash.empty(); // kosongkan semua trashTidak perlu setup — setiap Model.remove() otomatis log ke trash untuk model yang menggunakan useSoftDelete().
Model
import { Model } from 'kancil-framework';
export default class Todo extends Model {
static table = 'todos';
static { this.useSoftDelete(); } // enable soft delete
static async list(page) {
return this.paginateWhere(page, 10, {}, { orderBy: 'created_at DESC' });
}
}| Method | Description |
|--------|-------------|
| find(id) | Find by primary key |
| findAll(opts) | All records |
| findWhere(where, opts) | Filtered |
| create(data) | Insert |
| update(id, data) | Update |
| remove(id) | Soft/hard delete (smart) + auto Trash log |
| restore(id) | Restore soft-deleted |
| paginate(sql, page, perPage) | Pagination |
| paginateWhere(page, perPage, where, opts) | Pagination with filter |
| search(q, page, perPage, fields) | Search with LIKE |
| upsert(data) | Create or update |
Opsi: { withDeleted: true } untuk include deleted, { $deleted: 'only' } untuk hanya deleted.
CSRF
Stateless time-based token. Setiap form WAJIB:
<input type="hidden" name="_csrf" value="<!-- csrf-token -->">Auto-injected oleh framework di response body. Middleware csrf:
import csrf from './middleware/csrf.js';
app.post('/save', csrf, handler);Session
In-memory session, lazy-init.
req.state.session.get('key');
req.state.session.set('key', val);
req.state.session.setFlash('success', 'Berhasil!');
// di template: {{flash.success}}Validator
import { Validator } from 'kancil-framework';
const v = await Validator.make(data, { title: 'required|min:3|max:255' }, {
'title.min': 'Minimal 3 karakter'
});
v.pass → boolean
v.errors → { field: 'message' }
v.data → sanitized dataRules: required, min:n, max:n, email, numeric.
Rate Limiter
import { RateLimiter } from 'kancil-framework';
app.use(RateLimiter.middleware(100, 60)); // 100 req/menit global
app.get('/ping', RateLimiter.middleware(5, 30)); // 5 req/30 detik per routeCache
FIFO cache (2000 slot). Otomatis cache GET text/html 200 tanpa flash. Manual:
req.state.cache.set(key, data);
req.state.cache.get(key);
req.state.cache.clear();Other Features
| Import | Description |
|--------|-------------|
| Storage | Storage.disk.put/get/delete, Storage.s3 |
| Paginator | paginate(query, page, perPage) |
| Cart | Cart(session), .add(), .items(), .total(), .clear() |
| Api | Api.success(data), Api.error(msg, code) |
| Http | Fetch wrapper: Http.base(url).get('/path') |
| Event | Event.on(), Event.emit(), Event.off() |
| Mailer | Mailer.send({ to, subject, text, html }) via sendmail |
| Str | Str.ulid(), Str.uuid(), Str.slug(), Str.random() |
| Crypt | AES-256-GCM encrypt/decrypt via APP_KEY |
| Debug | dump(), dd(), bench(), benchAsync() |
| Logger | Logger.info(), Logger.error() ke file + console |
| Pdf | Pdf.make(doc => { doc.text(...) }) |
| Qr | Qr.toDataURL(text), Qr.toBuffer(text) |
| Barcode | Barcode.toBuffer(text, { format }) |
| Migration | Migration.run(dir) — migrate up |
| Seeder | Seeder.run(dir) — seed data |
| FileValidator | FileValidator.validate(file, rules) — mime, size, image |
Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| PORT | 3000 | Server port |
| DATABASE_URL | sqlite://:memory: | MySQL: mysql://user:pass@host:3306/db |
| APP_KEY | — | Wajib. Untuk CSRF & Crypt (min 32 char) |
| THEME | default | Nama theme folder |
| APP_ENV | development | production — disable cache busting |
| CSRF_TIMEOUT | 60 | CSRF token expiry (detik) |
| SESSION_TTL_HOURS | 24 | Session lifetime (jam) |
| CACHE_MAX | 2000 | Max cache slots |
| STORAGE_PATH | storage/app | Local storage base path |
Client-side JS
Framework menyediakan 2 file JS yang otomatis di-serve:
| File | Description |
|------|-------------|
| /kancil-ui.js | UI components — toast, dialog, confirmForm, loading. Auto-inject di <head> |
| /datatable.js | DataTable client — load, sort, search, bulk delete. Auto-inject di <head> |
| /kancil.css | Stylesheet untuk Kancil components. Auto-inject |
kancil-client.js juga tersedia (/kancil-client.js) — state management, AJAX helper, auth, cart, cache, validate, bind — load manual jika dibutuhkan.
License
MIT
