Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / Node.js

Model-driven REST or GraphQL API for CRUD and More

5.00/5 (23 votes)
11 May 2022MIT7 min read 52.3K   493  
Generic REST or GraphQL endpoints for CRUD and more, written in JavaScript, using Node.js, Express, and PostgreSQL
This article introduces a model-driven REST or GraphQL API for CRUD (Create, Read, Update, Delete) with which you can write simple models and the REST endpoints for CRUD will become available automatically.

Introduction

This article introduces a model-driven REST or GraphQL API for CRUD (Create, Read, Update, Delete). With it, you can write simple models (specifying a database table and the set of columns to be exposed) and the REST endpoints for CRUD will become available automatically. No hand-coding of any SQL is necessary.

The concept could be implemented on different technology stacks and languages. Here, I used JavaScript (which generates SQL) with Node.js, Express, and PostgreSQL.

Background

Most projects need to Create, Read, Update, and Delete objects. When these objects are simple enough (one driving table and a few columns in the database), the code is very similar from one object to the next. In fact, the patterns are the same, and the only differences are the names of the tables and the names and types of the columns.

Of course, there will always be complex endpoints which need to be written by hand but by automating the simple ones, we can save a lot of time.

Image 1

Using the Code

The sample database provides three examples for a To-Do list, an Addressbook, and a Graphic Novels inventory. These examples use object IDs "todo", "contact", and "comics", as specified in their models.

After playing with them, change the database (in config.js), change the models (in the models directory), and the RESTful API will change according to your data structure.

Follow these steps to install and setup the project with the sample database.

Installation

Download or clone from GitHub.

# To get the latest stable version, use git from the command line.
git clone https://github.com/evoluteur/evolutility-server-node

or use the npm package:

# To get the latest stable version, use npm from the command line.
npm install evolutility-server-node

Setup

After installing Evolutility-Server-Node, follow these steps:

  1. Create a PostgreSQL database.
  2. In the file config.js, set the PostgreSQL connection string and the schema name to access your new database.
  3. In the command line, type the following:
    # Install dependencies
    npm install
    
    # Create sample database w/ demo tables
    node js/setup/database.js
    
    # Run the node.js server
    npm start

In a web browser, go to the URL http://localhost:3000/api/v1/evolutility/todo.

Configuration

In the root directory, edit the file "config.js" to set the database connection and other options like paging and upload directory.

Models

To be accessible by the REST API, each database table must be described in a model. Models contain the name of the driving table and the list of fields/columns present in the API.

Entity

Property Meaning
id Unique key to identify the entity (used as API parameter)
table Database table name
fields Array of fields
titleField Field id for the record title
searchFields Array of field ids for fields used to perform searches

Field

Property Meaning
id Unique key for the field (can be the same as column but doesn't have to be)
column Database column name for the field
lovtable Table to join to for field value (only for fields of "lov" type)
lovcolumn Column name (in the lovtable) for field value (only for fields of "lov" type)
type Field type is not a database column type but more a UI field type. Possible field types:
  • boolean (yes/no)
  • date
  • datetime
  • decimal
  • document
  • email
  • image
  • integer
  • lov (list of values)
  • money
  • text
  • textmultiline
  • time
  • url
readonly Prevents field modification
inMany Determines if the field is present (by default) in lists of records

Notes: More field properties (unique, min, max, minLength, maxLength...) will be added later.

Sample Model

Here is a model for a To-Do app.

JavaScript
module.exports = {
    id: "todo",
    table: "task",
    titleField: "title",
    searchFields: ["title", "duedate", "description"],
    fields: [
        {
            id: "title", 
            column: "title", 
            type: "text", 
            inMany: true
        },
        {
            id: "duedate", 
            column: "duedate", 
            type: "date", 
            inMany: true
        },
        {
            id: "category", 
            column: "category_id", 
            type: "lov", 
            lovtable: "task_category",
            inMany: true
        },
        {
            id: "priority", 
            column: "priority_id", 
            type: "lov", 
            lovtable: "task_priority", 
            required: true, 
            inMany: true
        {
            id: "complete", 
            column: "complete", 
            type: "boolean", 
            inMany: true
        },
        {
            id: "description",  
            column: "description", 
            type: "textmultiline"
        }
    ]
};

This model only covers the backend. It is also possible to modelize the front-end (as long as the UX pattern is simple enough or it becomes so complex that it is not worth it). You can read more about it in my previous article, Minimalist Meta-Model for CRUD Applications.

REST API

The API of Evolutility-Server-Node is heavily inspired (even partially copied) from the one of PostgREST which also provides generic CRUD but directly inspects the database schema instead of using models.

When running the project locally, the URL for the "todo" app is http://localhost:3000/api/v1/evolutility/todo.

Requesting Information

Get One

To get a specific record by ID, use "< ObjectName >/ID".

GET /<object>/<id>

GET /todo/12

Get Many

Every model is exposed. You can query lists of items by using the model ID.

GET /<object>

GET /todo

Filtering

You can filter result rows by adding conditions on fields, each condition is a query string parameter.

GET /<object>/<field.id>=<operator>.<value>

GET /todo?title=sw.a
GET /todo?priority=in.1,2,3

Adding multiple parameters conjoins the conditions:

todo?complete=0&duedate=lt.2017-01-01

These operators are available:

Operator Meaning Example
eq equals /todo?category=eq.1
gt greater than /todo?duedate=gt.2017-01-15
lt less than /todo?duedate=lt.2017-01-15
gte less than or equal /todo?duedate=gte.2017-01-15
lte less than or equal /todo?duedate=lte.2017-01-15
ct contains /todo?title=ct.e
sw start with /todo?title=sw.a
fw finishes with /todo?title=fw.z
in one of a list of values /todo?priority=in.1,2,3
0 is false /todo?complete=0
1 is true /todo?complete=1
null is null /todo?category=null
nn is not null /todo?category==nn

Ordering

The reserved word "order" reorders the response rows. It uses a comma-separated list of fields and directions:

GET /<object>?order=<field.id>.<asc/desc>

GET /todo?order=priority.desc,title.asc

If no direction is specified, it defaults to ascending order:

GET /todo?order=duedate

Limiting and Pagination

The reserved words "page" and "pageSize" limits the response rows.

GET /<object>?page=<pageindex>&pageSize=<pagesize>

GET /todo?page=0&pageSize=50

Formatting

By default, all APIs return data in JSON format. This API call allows to request data in CSV format (export to Excel). This feature is using express-csv.

GET /<object>?format=csv

GET /todo?format=csv

Notes: In the returned data, every object has an extra property "_full_count" which indicates the total number of records in the query (before limit).

Updating Data

Record Creation

To create a row in a database table, post a JSON object whose keys are the names of the columns you would like to create. Missing keys will be set to default values when applicable.

POST /todo
{ title: 'Finish testing', priority: 2}

Update

PATCH /todo
{ title: 'Finish testing', priority: 2}

Deletion

Simply use the DELETE verb with the id of the record to remove.

DELETE /<object>/<id>

DELETE /todo/5

Extras Endpoints

In addition to CRUD, Evolutility-Server-Node provides endpoints for common UI needs like Charts and Lists of values.

Discovery

Returns the list of Objects and their APIs (only objects flagged active are included).

GET /

Note: This end-point must be enabled in the configuration with {apiInfo: true}.

Charts

For charts data, it is possible to get aggregated data.

GET /<object>/chart/<field id>

GET /todo/chart/category

Lists of Values

Dropdown fields in the UI (field.type="lov" in the model) have a REST endpoint to get the list of values for dropdowns.

GET /<object>/lov/<field id>

GET /todo/lov/category

Statistics

Returns the total count, and the min, max, average, and total for numeric fields in the model.

GET /<object>/stats

GET /todo/stats

File Upload

This endpoint lets you upload a file. The current (naive) implementation only saves the file on the file server in a folder named like the object id.

POST /<object>/upload/<id>

POST /comics/upload/5

With Query parameters: file and "fieldid".

Nested collections

If the model has collections defined, they can be queried with this end-point.

GET /<model.id>/collec/<collection.id>?id=<id>

GET /winecellar/collec/wine_tasting?id=1</code>

API Version

This endpoint gets the API version (as specified in the project's package.json file).

GET /version

GraphQL

Evolutility-Server-Node provides a GraphQL interface based on the same models as the REST API.

When running the project locally, the URL for garphiQL is http://localhost:2000/graphql.

Requesting Information

Get One record by Id

For getting a single record by Id.

JavaScript
{ 
    contact (id: 1 ){
        firstname
        lastname
        category_txt
        email
  }
}

Get Many records

All objects are exposed for queries with search and filters. Filter use the same syntax for conditions as the REST API (for example: { firstname: "sw.A" } for "Firstname starts with "A").

Fields of type "lov" (List of values) are represented as 2 fields for Id and value.

JavaScript
{ 
	urgent_tasks: todos ( complete: "false", priority: "lt.3" ){
	    title
	    description
	    priority
	    priority_txt
	    category
	    category_txt
	    complete
  }
  	ab_a_contacts: contacts (search: "ab", firstname: "sw.A") { 
	    id
	    firstname
	    lastname
	    category_txt
	    email
  }
}

Charts Data

For all objects records can be aggregated and counted by field (for fields of numeric or "lov" types).

JavaScript
{ 
  contacts_by_category: contact_charts(fieldId:"category"){
    label 
    value
  }
  task_by_priority: todo_charts(fieldId:"priority") {
    label 
    value
  }
  restaurants_by_cuisine: restaurant_charts(fieldId:"cuisine") {
    label 
    value
  }
}

Points of Interest

The Model Driven Architecture (MDA) will get you up-and-running faster but also save you time in the evolutionary maintenance of your project.

For example, when the project is done, we often need to add fields. Normally, that means adding a column to the database, and manually adding the field in every REST endpoint which uses the database table. With the model-driven approach, once the column is added to the database, you only need to add the field in a single place (the model) and every endpoint using that model will expose it.

The model-driven approach can also be applied to UI. If you want to play with it, I made two different implementations of a matching model-driven UI, Evolutility-UI-React (for React) and Evolutility-UI-jQuery (for jQuery and BackboneJS).

History

  • 15th February, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License