Generate Express CRUD Routes from a Mongoose Model

CRUD might be the most aptly named acronym in programming jargon. Short for Create, Read, Update, Delete; CRUD is arguably the most routine part of backend development.

In this tutorial we are going to configure Optic to automatically generate/maintain the CRUD routes in our backend.

Requirements

I've included the source in a demo todo app project to make it easy to try this out. Download it here.

Including Skills

To get started we need to include some basic Javascript skills maintained by the Optic team. You can do this by updating the optic.yml file in your project's root directory.

We'll be adding 3 skill packages:

  1. optic:rest - Contains the common schemas for endpoints, parameters, headers, and responses.
  2. optic:express-js - The skills used for interfacing with express routes
  3. optic:mongoose - Skills for interfacing with mongoose queries, schemas and models.

Once added, your updated optic.yml file should look something like this:

name: Mongoose CRUD Demo

parsers:
  - es7

skills:
  - optic:express-js@0.3.0
  - optic:rest@0.3.0
  - optic:requestjs@0.3.0
  - optic:mongoose@0.3.0

exclude:
  - "node_modules"

Once you've updated your optic.yml file you can start up your favorite IDE and Optic to get started.

Our First Transform

Because you have included the optic:mongoose skill in your Optic configuration Optic can now read every mongoose model in your project.

Add the following todo model definition in your app. When you select this code, Optic will light up. Open Optic by clicking on the glowing dot or CMD+Tab to the app.

export const ToDo = mongoose.model('Todo', new mongoose.Schema({
  task: 'string',
  isDone: 'boolean'
}));

You'll see that Optic interprets the above code as follows:

{
	"name": "ToDo",
	"schema": {
		"task": "string",
		"isDone": "boolean"
	}
}

Because mongoose schema definitions are so verbose it isn't difficult to see how Optic extracted this model from the raw source code.

In most CRUD generators you have to provide a Swagger description of all your models/routes or manually outline all the models, properties and relationships in your database using a CLI. Because Optic can read your code on its own it can generate your CRUD routes by using your code as the input.

This makes Optic easier to use and much more flexible than a one-shot code generator. There's also the advantage of being able to have Optic update the generated CRUD routes whenever your schemas are changed.

Now let's apply a transformation to turn this model into a request.

A transformation is a pure function that takes a JSON object that conforms to Schema A as input and returns a JSON object that conforms to Schema B.

In this case our transformation takes the JSON object above that represents a mongoose model and returns a JSON Object that represents a CRUD endpoint with a query inside.

  • Put your cursor over the todo model
  • Go to the transformations tab within Optic's GUI (the blue plus button at the bottom).
  • Click on the transformation called "Create Route"

The following code will appear beneath your model:

app.post('/todo', (req, res) => {  //name: Create Todo Route, source: Todo Model -> optic:mongoose/createroutefromschema {}
  new ToDo({ task: req.body.task, isDone: req.body.isDone }).save((err, item) => {  //tag: query
    if (!err) {
        res.status(200).send(item)
    } else {
        res.status(400).send(err)
    }
  })
})

As you can see Optic has created a post route to /todo that accepts a JSON body with a task and isDone parameter. Within the handler for that route Optic creates a query that creates and saves a new instance of ToDo in the database.

Now try generating the other CRUD Routes using the available transformations in the mongoose skill.

^Remember: Since you're using Optic you can edit the generated code to add your own custom responses, errors and validation. Optic will still be able to help you maintain it over time.

Naming Code Objects

Generating CRUD routes like this is very useful, but to take full advantage of Optic's power we need to start recording the relationships between our models and the CRUD routes we generate.

We can do this by adding a name annotation to our mongoose model.

export const ToDo = mongoose.model('Todo', new mongoose.Schema({ //name: Todo Model
  task: 'string',
  isDone: 'boolean'
}));

Now when you generate a CRUD route it'll have a source annotation indicating that it's the result of "Todo Model" being transformed.

app.post('/todo', (req, res) => {  //source: Todo Model -> optic:mongoose/createroutefromschema {}
  new ToDo({ task: req.body.task, isDone: req.body.isDone }).save((err, item) => {  //tag: query
    if (!err) {
        res.status(200).send(item)
    } else {
        res.status(400).send(err)
    }
  })
})

Name and Source Annotations

One of Optic's key design principles is making code the ultimate source of truth. We don't want to hide any information from the end user so when it comes to storing relationships between different sections of code we've elected to use annotations.

There are two kinds of annotations:

  1. Name Annotations assign a name to a section of code. This provides a developer friendly way of referencing the models Optic finds in your code. There's a single project level namespace for these names so it's important to make them unique. {Name} + {Type} ie "Create User Endpoint" is a good pattern to follow
  2. Source Annotations record the model and the transformation used to generate this section of code.

Syncing Models & CRUD Routes

No matter how well you plan your database, changes will need to be made to your schemas and CRUD routes. Traditional code generators are no help here, but since Optic can both read/write code you can use it maintain the generated code over time.

Remember all those name and source annotations? Optic uses those to create a acyclic graph representing the internal dependencies of your code. Each named object becomes a node and the source annotations become edges between them. When you press "Sync" in Optic it will diff the graph representing your current code base with the expected graph.

To see how this works let's add a dueDate field to our Todo model.

export const ToDo = mongoose.model('Todo', new mongoose.Schema({ //name: Todo Model
  task: 'string',
  isDone: 'boolean',
  dueDate: 'date'
}));

Head over to the Optic GUI and click "Sync". Optic will generate a patch for your CRUD routes.

Image of Patch

The new CRUD route should look like this. Notice how it only changed the line of the query to add the new field. Any custom code you wrote in the handler is left alone during a sync.

app.post('/todo', (req, res) => {  //source: Todo Model -> optic:mongoose/createroutefromschema {}
  new ToDo({ task: req.body.task, isDone: req.body.isDone, dueDate: req.body.dueDate }).save((err, item) => {  //tag: query
    if (!err) {
        res.status(200).send(item)
    } else {
        res.status(400).send(err)
    }
  })
})

Click "Apply" and patches will be applied and written to disk.

Going Further

If you want to use another database, ORM or REST API library you can learn more about teaching Optic to work with new types of code in our docs.

We also have a tutorial for syncing your client networking code with the routes in your backend. Now that you've learned how to generate all your CRUD routes it might be worth checking out.

Thanks for reading! We hope this tutorial helps you get your apps off the ground quickly!

  1. Generate RequestJS calls for every route in an Express backend
    1. Generate RequestJS calls for every route in an Express backend
AUTHOR: Optic Team