What is Remult?
Remult is a full-stack CRUD framework that uses your TypeScript model types to provide:
- Secure REST API (highly configurable)
- Type-safe frontend API client
- Type-safe backend query builder
Remult โค๏ธ Code Sharing
With model types shared between frontend and backend code, Remult can enforce data validation and constraints, defined once, both in the front-end and within back-end API routes.
Getting started
The best way to learn Remult is by following a tutorial of a simple Todo web app with a Node.js Express backend.
Installation
npm i remult
Usage
Setup API backend using a Node.js Express middleware
import express from 'express';
import { remultExpress } from 'remult/remult-express';
const port = 3001;
const app = express();
app.use(remultExpress());
app.listen(port, () => {
console.log(`Example API listening at http://localhost:${port}`);
});
Define model classes
import { Entity, Fields } from 'remult';
@Entity('products', {
allowApiCrud: true
})
export class Product {
@Fields.string()
name = '';
@Fields.number()
unitPrice = 0;
}
๐ API Ready
> curl http://localhost:3001/api/products
[{"name":"Tofu","unitPrice":5}]
Find and manipulate data in type-safe frontend code
async function increasePriceOfTofu(priceIncrease: number) {
const productsRepo = remult.repo(Product);
const product = await productsRepo.findFirst({ name: 'Tofu' });
product.unitPrice += priceIncrease;
productsRepo.save(product);
}
...exactly the same way as in backend code
@BackendMethod({ allowed: Allow.authenticated })
static async increasePriceOfTofu(priceIncrease: number, remult?: Remult) {
const productsRepo = remult!.repo(Product);
const product = await productsRepo.findFirst({ name: 'Tofu' });
product.unitPrice += priceIncrease;
productsRepo.save(product);
}
โ๏ธ Data validation and constraints - defined once
import { Entity, Fields } from 'remult';
@Entity('products', {
allowApiCrud: true
})
export class Product {
@Fields.string<Product>({
validate: product => {
if (product.name.trim().length == 0)
throw 'required';
}
})
name = '';
@Fields.number({
validate: (_, field) => {
if (field.value < 0)
throw "must not be less than 0";
}
})
unitPrice = 0;
}
Enforced in frontend:
const product = productsRepo.create();
try {
await productsRepo.save(product);
}
catch (e: any) {
console.error(e.message); // Browser console will display - "Name: required"
}
Enforced in backend:
> curl http://localhost:3001/api/products -H "Content-Type: application/json" -d "{""unitPrice"":-1}"
{"modelState":{"unitPrice":"must not be less than 0","name":"required"},"message":"Name: required"}
๐ Secure the API with fine-grained authorization
@Entity<Article>('Articles', {
allowApiRead: true,
allowApiInsert: remult => remult.authenticated(),
allowApiUpdate: (remult, article) => article.author.id == remult.user.id
})
export class Article {
@Fields.string({ allowApiUpdate: false })
slug = '';
@Field(() => Profile, { allowApiUpdate: false })
author!: Profile;
@Fields.string()
content = '';
}
Example App
CRM demo with a React + MUI front-end and Postgres database.
Contributing
Contributions are welcome. See CONTRIBUTING.
License
Remult is MIT Licensed.