The marmelab blog

Introducing admin-on-rest, the React Admin GUI for REST APIs

Published on 2 September 2016 by François Zaninotto with tags admin-on-rest ng-admin ReactJS open source

For two years, we’ve been developing admin GUIs on top of REST APIs using ng-admin. During the same period, we’ve progressively switched from Angular.js to React.js as our framework of choice for Single Page Applications. The logical next step was to rewrite ng-admin from the ground up in React.js. That’s what admin-on-rest is; it’s open-source, and it’s awesome.

Admin GUI for REST APIs, using Material UI

Admin-on-rest uses the same architecture as ng-admin: it’s a single-page application that relies on a REST API for persistence. It provides the views required to Create, Retrieve, Update, and Delete (CRUD) REST resources.

The user interface is different, though. While ng-admin used Bootstrap, admin-on-rest bets on Material Design. This results in a more modern look, full of small transitions and animations that make the user experience smoother.

Just like ng-admin, admin-on-rest supports multiple field types, multiple entities, relationships, and lets developers override pretty much anything they want with components of their own. Except it’s now more natural, because the configuration API uses React Components.

Redesigned Configuration API

For ng-admin, we invented a Domain Specific Language (DSL) to describe admin GUIs in JavaScript. We called that DSL admin-config. If you’ve used ng-admin, this configuration for the list view of a comment entity should look familiar:

(function() {
    var app = angular.module('myApp', ['ng-admin']);
    app.config(['NgAdminConfigurationProvider', function (NgAdminConfigurationProvider) {
        var nga = NgAdminConfigurationProvider;
        var admin = nga.application('ng-admin backend demo') // application main title
            .baseApiUrl('http://localhost:3000/'); // main API endpoint
        var comment = nga.entity('comments')
            .baseApiUrl('http://localhost:3000/');
        admin.addEntity(comment);
        comment.listView()
            .title('Comments')
            .fields([
                nga.field('id'),
                nga.field('post_id', 'reference')
                    .label('Post')
                    .targetEntity(post)
                    .targetField(nga.field('title').map(truncate))
                    .singleApiCall(ids => { return {'id': ids }; }),
                nga.field('created_at', 'date').label('Date'),
            ])
            .filters([
                nga.field('post_id', 'reference')
                    .label('Post')
                    .targetEntity(post)
                    .targetField(nga.field('title'))
            ]);
    });
}());

Although relatively easy to read, this configuration API mixed declarative and imperative styles. It relied heavily on the ability to turn strings into components, and on a custom “God” object (nga) that often confused newcomers.

For the React version, we’ve switched to a fully declarative approach. To specify views and fields, you just use custom React components, so the equivalent of the previous code snippet would be:

import React from 'react';
import { render } from 'react-dom';
import { simpleRestClient, Admin, Resource } from 'admin-on-rest';
import { List, Filter, DateField, ReferenceField, TextField, EditButton, ReferenceInput } from 'admin-on-rest/mui';

const CommentFilter = (props) => (
    <Filter {...props}>
        <ReferenceInput label="Post" source="post_id" reference="posts" referenceSource="title" allowEmpty />
    </Filter>
);

export const CommentList = (props) => (
    <List title="All comments" {...props} filter={CommentFilter}>
        <TextField label="id" source="id" />
        <ReferenceField label="Post" source="post_id" reference="posts" referenceSource="title" />
        <DateField label="date" source="created_at" />
        <EditButton />
    </List>
);

render(
    <Admin restClient={simpleRestClient('http://localhost:3000')}>
        <Resource name="comments" list={CommentList} />
    </Admin>,
    document.getElementById('root')
);

If you’re familiar with React, you will feel perfectly at home with admin-on-rest.

The Power and Simplicity of React

The real bonus of the declarative approach is that whenever you need to use a custom presentation or fetch logic, you can just use plain React components.

For instance, admin-on-rest fields are simple React components. At runtime, they receive the record they operate on (coming from the API, e.g. { "id": 1, "name": "Leanne Graham", "username": "Bret", "email": "[email protected]" }), and the source field they should display (e.g. ‘email’). That means that the code for a custom field component is really simple. here is an example for an email field:

import React, { PropTypes } from 'react';

const EmailField = ({ record = {}, source }) => <a href={`mailto:${record[source]}`}>{record[source]}</a>;

EmailField.propTypes = {
    source: PropTypes.string.isRequired,
    record: PropTypes.object,
};

export default EmailField;

Creating your own custom fields shouldn’t be too difficult.

REST API Mapping Moved To A Simple Function

Ng-admin took advantage of the popular Restangular library to let developers customize their REST dialect (mostly using interceptors). But Restangular was too tighly coupled with Angular.js to be used in a React project. So at first, we decided to build a framework-agnostic version of Restangular, which we released under the name Restful.js.

But it turns out that with ES5 and the introduction of fetch(), all it takes to make an REST API call is a function call. So we decided that it’s the developer’s responsibility to provide a simple function that turns a REST request into an HTTP request. And it makes things extremely straightforward. Here is an example API mapping function for admin-on-rest:

// in src/restClient
import { GET_LIST, GET_ONE } from 'admin-on-rest';
const API_URL = 'my.api.url';

/**
 * @param {String} type One of the constants appearing at the top if this file, e.g. 'GET_LIST'
 * @param {String} resource Name of the resource to fetch, e.g. 'posts'
 * @param {Object} params The REST request params, depending on the type
 * @returns {String} url The url for the HTTP request
 */
const convertRESTRequestToHTTP = (type, resource, params) => {
    let url = '';
    const { queryParameters } = fetchUtils;
    switch (type) {
    case GET_LIST: {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
            filter: JSON.stringify(params.filter),
        };
        url = `${API_URL}/${resource}?${queryParameters(query)}`;
        break;
    }
    case GET_ONE:
        url = `${API_URL}/${resource}/${params.id}`;
        break;
    default:
        throw new Error(`Unsupported fetch action type ${type}`);
    }
    return url;
};

/**
 * @param {Object} response HTTP response from fetch()
 * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
 * @returns {Object} REST response
 */
const convertHTTPResponseToREST = (response, type) => {
    const { headers, json } = response;
    switch (type) {
    case GET_LIST:
        return {
            data: json.map(x => x),
            total: parseInt(headers['content-range'].split('/').pop(), 10),
        };
    default:
        return json;
    }
};

/**
 * @param {string} type Request type, e.g GET_LIST
 * @param {string} resource Resource name, e.g. "posts"
 * @param {Object} payload Request parameters. Depends on the request type
 * @returns {Promise} the Promise for a REST response
 */
export default (type, resource, params) => {
    const { fetchJSON } = fetchUtils;
    return fetchJson(convertRESTRequestToHTTP(type, resource, params))
        .then(response => convertHTTPResponseToREST(response, type));
};

In addition, admin-on-rest already provides mappers for various REST dialects. It’ll be extremely simple to support additional REST dialects in the future (like JSON API or HAL).

You Just Need To Know React

Admin-on-rest can be used as a standalone application. If you know React, you should be able to build sophisticated admin GUIs just by following the documentation. We’ve written a Getting started tutorial based on react-create-app that should get you up and running in 20 minutes.

Documentation is an ongoing effort at this time. Don’t hesitate to dig the source, it’s usually self-explanatory (thanks to React PropTypes and dumb components).

Built On The Shoulders Of Giants

Admin-on-rest wouldn’t be possible without redux, react-router, redux-saga, and material-ui. We’ve carefully chosen these dependencies so that developers can easily replace the parts that they don’t like, following the “batteries included but removable” guidelines.

In fact, most of the admin-on-rest code is made of redux reducers and material-ui components. The ES6 code is extremely simple (especially compared to the ng-admin code), takes advantage of modern JavaScript good practices, and should be a pleasure to work with.

What About react-admin?

You may know that it’s not our first attempt at redeveloping ng-admin for React.js. We initially worked a project called react-admin, using the same configuration API as ng-admin, and a pre-redux Flux implementation.

Some of the code was not idiomatic React, but a literal port of the angular architecture. Implementing complex features (such as relationships) proved challenging. Besides, it used the same GUI as ng-admin (bootstrap), while we wanted to try Material UI. We decided not to reuse any of this code and go for a full rewrite instead.

React-admin will be abandoned, and our efforts redirected to admin-on-rest instead.

Will We Continue To Support ng-admin?

We still have many customer projects in production using ng-admin, for which we continue to develop new features. So if you’re using ng-admin, too, rest assured: we’ll continue to support it, fix bugs, and improve its performance over time.

However, don’t expect large refactorings or huge improvements (like an Angular 2 port): most of our work will now focus on admin-on-rest.

Conclusion

We’ll use the same recipes that made the success of ng-admin: great documentation, plenty of examples, focus on developer experience, and active support. We hope that admin-on-rest will have the same reception, and the same large user base. It’s built for the future.

We’re in the process of updating our boilerplate to use admin-on-rest instead of ng-admin. We’ll also build a demo similar to Posters Galore to showcase the features of the framework.

We hope you’ll love admin-on-rest as much as we do. Please give it a try, and give us your feedback.

comments powered by Disqus