DEV Community

Jan Küster
Jan Küster

Posted on

Easy CRUD setup with Meteor in only 4 steps from scratch

Meteor has probably the fastest and easiest way from installation to running a CRUD app out there: install, create project, tweak the boilerplate ui a bit, done! 😲

This is, because Meteor's devmode contains a package insecure that allows DB writes from the client, making prototyping new ideas super fast! 🔥

However, you will have to implement a real CRUD system, if things get more serious than just playing with the examples. Meteor will also provide a fast and reliable way to achieve this goal. Let me guide you through it. All you need to start is to open your console and follow the instructions. 🫡


Recap - what is CRUD?

As you might already know, CRUD stands for Create - Read - Update - Delete. They are the basic types of operations on a storage system.

Meteor uses MongoDB as storage system, which groups documents in so called collections. CRUD operations are implement in Mongo via insert (create), find (read), update (update) and remove (delete).

Implementing a CRUD system can be a good starting point for a new app idea but is also a common part many more complex setups.

Now let's get started from scratch.


1. Install Meteor and create a new project

Installing the latest Meteor version is just a single line away:

For Windows, Linux and OS X, you can run the following command (Node.js 14.x is required):

npm install -g meteor
Enter fullscreen mode Exit fullscreen mode

An alternative for Linux and OS X, is to install Meteor by using curl:

curl https://install.meteor.com/ | sh
Enter fullscreen mode Exit fullscreen mode

Once you have installed Meteor you can create a new project via

meteor create crud-app
cd crud-app
meteor
Enter fullscreen mode Exit fullscreen mode

You can open your browser and navigate to localhost:3000 to confirm it's all running properly. That's it for the installation part.


2. Add CRUD functionality to the UI

The default meteor create <projectname> uses React as frontend but you can also chooser a different frontend, like Vue, Svelte, Blaze or others. Run meteor create --help to get a list of all available options.

With the React frontend you will also get a minimal app skeleton. Let's enhance the file imports/ui/Info.jsx a bit, to support all CRUD operations:

import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { LinksCollection } from '../api/links';

export const Info = () => {
  const [formTarget, setFormTarget] = useState(null);
  const links = useTracker(() => {
    return LinksCollection.find().fetch();
  });

  const renderLinkForm = () => {
    return formTarget
      ? (<LinkForm onSubmitted={() => setFormTarget(null)} doc={formTarget.doc} type={formTarget.type} />)
      : null
  };

  return (
    <div>
      <h2>Learn Meteor!</h2>
      <ul>{links.map(
        link => <li key={link._id}>
          <a href={link.url} target="_blank">{link.title}</a>
          <button onClick={() => setFormTarget({ type: 'update', doc: link })}>Update</button>
          <button onClick={() => LinksCollection.remove({ _id: link._id })}>Delete</button>
        </li>
      )}</ul>
      {renderLinkForm()}
      <button onClick={() => setFormTarget({ type: 'insert' })}>Create new</button>
    </div>
  );
};

const LinkForm = ({  type, doc, onSubmitted  }) => {
  const [title, setTitle] = useState(doc?.title ?? '');
  const [url, setUrl] = useState(doc?.url ?? '');

  const onSubmit = () => {
    let result;
    if (type === 'insert') {
      result = LinksCollection.insert({ title, url });
    }
    if (type === 'update') {
      result = LinksCollection.update(doc._id, { $set: { title, url } });
    }
    onSubmitted(result);
  };

  return (
    <form onSubmit={onSubmit}>
      <label>
        <span>Title</span>
        <input type="text" value={title} onChange={e => setTitle(e.target.value)} />
      </label>
      <label>
        <span>URL</span>
        <input type="text" value={url} onChange={e => setUrl(e.target.value)} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Meteor will automatically reload the app in your browser. It should look like this now:

Image description

You can now play around with the links and see all the changes in your Mongo Collection on the server. To verify this is working, open a new terminal in your OS and enter the project folder, then enter the following commands:

$ meteor mongo
# wait for the Mongo shell to start
meteor:PRIMARY> db.links.find()
{ "_id" : "F8BfbeWEbnuRNds65", "title" : "dev.to", "url" : "https://dev.to" }

Enter fullscreen mode Exit fullscreen mode

It should list the updated collection, reflecting all your changes. That's it. A complete CRUD system. 🎉

Okay, not so fast. This is insecure as hell. 🔥 You can now close the Mongo shell and enter instead the following command:

$ meteor remove insecure
Enter fullscreen mode Exit fullscreen mode

Now try again to edit your collection. It will not work and the browser console will display an error Access denied [403] each time you try to operate on the collection.

This is, because now without the insecure package being present you will actually have to create endpoints on the server that your client has to call. In Meteor these are called Meteor Methods.

The good thing: it's just as easy as everything has been up to this point.


3. Implement the CRUD Methods on the server

Meteor Methods are endpoints that execute server-side code.
If you are new to Meteor you may later want to read more about Methods in the official guide.

Until that, let's create the Methods in our server startup file server/main.js:

import { Meteor } from 'meteor/meteor';
import { LinksCollection } from '/imports/api/links';

// ... keep the original code, 
// I just skip it here for better readability

Meteor.methods({
  'links.create': function ({ title, url }) {
    return LinksCollection.insert({ title, url })
  },
  'links.update': function ({ _id, title, url }) {
    const $set = {}
    if (title) { $set.title = title }
    if (url) { $set.url = url }
    return LinksCollection.update({ _id }, { $set })
  },
  'links.delete': function ({ _id }) {
    return LinksCollection.remove({ _id })
  }
})
Enter fullscreen mode Exit fullscreen mode

4. Make your client call the Methods

In the final step we only need to add two things to our client side code: 1. replace the insert/update/delete calls on LinksCollection with Meteor.call and 2. provide a simple error message, in case something went wrong on the server.

Here is the modified Info.jsx file:

import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { LinksCollection } from '../api/links';

export const Info = () => {
  const [formTarget, setFormTarget] = useState(null);
  const [error, setError] = useState(null);
  const links = useTracker(() => {
    return LinksCollection.find().fetch();
  });

  const onSubmitted = () => {
    setFormTarget(null);
    setError(null);
  };
  const onError = (err) => setError(err);

  const renderLinkForm = () => {
    return formTarget
      ? (<LinkForm onSubmitted={onSubmitted} onError={onError} doc={formTarget.doc} type={formTarget.type} />)
      : null
  };

  const renderError = () => {
    return error
      ? (<div>{error.message}</div>)
      : null
  }
  const remove = _id => {
    Meteor.call('links.delete', { _id }, (err) => setError(err || null))
  }

  return (
    <div>
      <h2>Learn Meteor!</h2>
      <ul>{links.map(
        link => <li key={link._id}>
          <a href={link.url} target="_blank">{link.title}</a>
          <button onClick={() => setFormTarget({ type: 'update', doc: link })}>Update</button>
          <button onClick={() => remove(link._id)}>Delete</button>
        </li>
      )}</ul>
      {renderLinkForm()}
      {renderError()}
      <button onClick={() => setFormTarget({ type: 'insert' })}>Create new</button>
    </div>
  );
};

const LinkForm = ({  type, doc, onSubmitted, onError  }) => {
  const [title, setTitle] = useState(doc?.title ?? '');
  const [url, setUrl] = useState(doc?.url ?? '');

  const onSubmit = e => {
    e.preventDefault()
    if (type === 'insert') {
      Meteor.call('links.create', { title, url }, (err, res) => {
        if (err) { return onError(err); }
        onSubmitted(res);
      })
    }
    if (type === 'update') {
      Meteor.call('links.update', { _id: doc._id, title, url }, (err, res) => {
        if (err) { return onError(err); }
        onSubmitted(res);
      })
    }
  };

  return (
    <form onSubmit={onSubmit}>
      <label>
        <span>Title</span>
        <input type="text" value={title} onChange={e => setTitle(e.target.value)} />
      </label>
      <label>
        <span>URL</span>
        <input type="text" value={url} onChange={e => setUrl(e.target.value)} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is our first basic CRUD setup. 🎉


What comes next?

At this point, there are still many things uncovered:

  • permissions check (authentication)
  • validate input
  • log errors on the server
  • implement the CRUD methods in a generic way and reuse them on any collection

Next week I will publish the follow-up article, where all these topics will be covered. At the end you will have a running CRUD system and become ready for some serious action.
We will continue from exactly this point.

If you became interested in Meteor and want to dive-in quickly I suggest you to visit the Meteor University course "Meteor 101: The Fundamentals"! It's free and full of information to help you bring your ideas to deployment fast and reliable.

Furthermore, you can connect with the Meteor community via various channels:

Disclaimer

I am not directly financially affiliated with Meteor Software or any former company behind Meteor. However, I am now an elected member of the Meteor Ambassador Program.

Discussion (0)