README
What is Remult?
Remult is a fullstack CRUD framework which uses your TypeScript model types to provide:
- Secured REST API (highly configurable)
- Type-safe frontend API client
- Type-safe backend query builder
Remult :heart: Monorepos
Using a monorepo
approach, with model types shared between frontend and backend code, Remult can enforce data validation and constraints, defined once, on both frontend and REST API level.
Getting started
The best way to learn Remult is by following a tutorial of a simple Todo app with a Node.js Express backend. There's one using a React frontend and one using Angular.
Installation
npm i remult
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, EntityBase, Field } from 'remult';
@Entity('products', {
allowApiCrud: true
})
export class Product extends EntityBase {
@Field()
name: string = '';
@Field()
unitPrice: number = 0;
}
:rocket: 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 product = await remult.repo(Product).findFirst({ name: 'Tofu' });
product.unitPrice += priceIncrease;
product.save();
}
...exactly the same way as in backend code
@BackendMethod({ allowed: Allow.authenticated })
static async increasePriceOfTofu(priceIncrease: number, remult?: Remult) {
const product = await remult!.repo(Product).findFirst({ name: 'Tofu' });
product.unitPrice += priceIncrease;
product.save();
}
:ballot_box_with_check: Data validation and constraints - defined once
import { Entity, EntityBase, Field } from 'remult';
import { Min } from 'class-validator';
@Entity('products', {
allowApiCrud: true
})
export class Product extends EntityBase {
@Field<Product>({
validate: p => {
if (p.name.trim().length == 0)
p.$.name.error = 'required';
}
})
name: string = '';
@Field()
@Min(0)
unitPrice: number = 0;
}
Enforced in frontend:
const product = remult.repo(Product).create();
try {
await product.save();
}
catch {
console.error(product._.error); // Browser console will display - "Name: required"
}
Enforced in backend:
> curl -d "{""unitPrice"":-1}" -H "Content-Type: application/json" -X POST http://localhost:3001/api/products
{"modelState":{"unitPrice":"unitPrice must not be less than 0","name":"required"},"message":"Name: required"}
:lock: 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 extends EntityBase {
@Field({ allowApiUpdate: false })
slug: string;
@Field({ allowApiUpdate: false })
author: Profile;
@Field()
content: string;
}