remult

remult core lib

Usage no npm install needed!

<script type="module">
  import remult from 'https://cdn.skypack.dev/remult';
</script>

README

Remult

GitHub license npm version

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;
}