How to build production apps with Meteor, typescript, React, and external integrations

Building a fully-featured blogging platform with Meteor 3.0 and ChatGPT in half an hour

Lightning-fast web application development with Meteor 3.0 book: Chapter 1

Anton Antich
Superstring Theory

--

You will learn: Meteor architecture; Interface — Schema — SmartMethods hierarchy; Architecture of a simple multi-user blogging platform using (sort-of) MVC approach; First version of the live blogging platform app; using ChatGPT to handle boring coding tasks for us.

Outline of this article:

  1. Introduction
  2. Meteor Architecture
  3. Installation and Blogging App Intro
  4. Model: Data Layer with Collections
  5. Controller: Meteor.Methods
  6. Schemas, Validation, Minimizing Boilerplate
  7. Better Controller: ValidatedMethods
  8. Users, Authentication, Authorization
  9. Final Controllers: Secure Post business methods
  10. Publish / Subscribe: getting to the View
  11. View: Unstyled React and React Router
  12. Final View: PubSub On The Client and Reactivity

The full source code for the application is available here:

Introduction

I’ve been involved with Meteor from the very beginning — at the time they already had “publish/subscribe”, but did not have authentication or authorization yet. Used it extensively on and off in proprietary corporate projects at Veeam Software, where I ran all of the operations and business infrastructure teams, as well as standalone platforms — such as innmind.com and anasaea.com, the one we are working on now. We have learned a lot while building those, and since I do love to share what I know and the major release of Meteor 3.0 is coming up very soon — I decided to start another post series to be turned into a book.

My other (almost finished) book on learning Type Theory and Haskell the fun way is here in case you are interested in a slightly off-beat path:

Along the course of this series we will look at and learn:

  • Why Meteor is cool and it’s architecture
  • Best practices in designing robust data-driven typed applications
  • How to minimize boilerplate through the use of libraries, (light) code generators, and ChatGPT (yes, it can help a lot)
  • How to use a practical, no-nonsense approach to authorization
  • How to build single-tenant and multitenant applications with integrations to Enterprise systems such as Microsoft Azure or Google
  • How to utilize OpenAI via their API
  • How to handle sales and subscriptions via Stripe
  • How to deploy and monitor production applications

By the end of the book, you should be able to become proficient in building pretty much any kind of practical data-driven web application in the shortest time possible. The book assumes familiarity with typescript and react — we won’t be going through the basics of the languages and programming per se.

Since the focus is on practicality, we won’t be going too wide, but will be going deep into the selected technology stack:

  • Meteor (3.0 where possible — it’s not out yet as of the time of this writing, but we need to be prepared),
  • React and react-bootstrap for the front-end,
  • Typescript as the language (because javascript is still the worst language in the universe after php)
  • Selected useful external integrations: Microsoft Azure, OpenAI, Stripe, and maybe some others such as Google and Facebook Graph — depending on the demand

Why Meteor and Meteor Architecture

You can read countless opinions and comparisons of Meteor to other technologies out there. The beauty and convenience of Meteor for us after building about 10 quite large-scale projects using it are in:

  • Write once, run anywhere (well, almost) — the same API both on the client and the server
  • Reactivity (invented before React by the way) naturally fits into React on the front-end
  • Absolutely seamless upgrades and no hassle with database migrations
  • Extremely fast time to market from an idea to a working useable solution, while no performance compromises (unlike countless low/no-code solutions)
  • Maybe the most important for those that love microservices and distributed architectures: Meteor will serve you faithfully as the “core application”, handling user management, data entities for a “single source of truth”, etc, while you can still integrate as many external APIs, microservices or not, as you like
  • Very good documentation and package ecosystem

Now let’s jump right into some of the key architecture details. To be as practical as possible from the very beginning, we will start our series by building a simple multi-user blogging platform and will learn key Meteor concepts along the way, which will become very useful as we move to much more complex applications development. The three key architectural building blocks, out of which we will build beautiful, robust, scalable applications are:

  • Collections
  • Publish / Subscribe
  • Methods

They will combine with React on the client to produce a nice MVC-ish architecture for apps of any complexity:

Blogging Platform: Type Foundation

We will use a data-driven typed approach, which means we have to start any project by defining the persistent data types we will need. Everything else will revolve around them. For the blogging platform, we will have the following:

  • User: represents a person that is a user of the system and can author Posts or Comments
  • Post: represents a blog post or an article
  • Comment: represents a comment on the blog post

In terms of functionality, we will have to provide support to the typical blogging platform features:

  • Register in the system
  • Write and edit blog posts
  • Read and subscribe to other users
  • Write and edit comments on blog posts

Let’s see how fast we can build it :)

Go and install Meteor if you haven’t done so yet, or simply run:

npm install -g meteor

At the time of this writing, Meteor 3.0 is not available yet so we will build the blogging app using the 2.12 version, but will upgrade it to Meteor 3.0 in future posts. Key concepts remain the same, the main difference of Meteor 3.0 is in moving towards standard javascript asynchronous support using async and promises from the fibers package used in Meteor 2 and below.

To create our blogging app, run:

meteor create --typescript blogplatform

This will create a bare app using typescript and react. Go into the imports/api folder and delete the generated links.ts file there. Then go into imports/ui and delete Hello.tsx and Info.tsx , then open App.tsx and make some edits so that it looks roughly like this:

import React from 'react';

export const App = () => (
<div>
<h1>Welcome to Meteor!</h1>
</div>
);

Finally, go to server/main.ts and delete everything, except for the meteor import line so that it looks like this:

import { Meteor } from 'meteor/meteor';

Now if you execute meteor in your project folder and navigate to localhost:3000 you should see “Welcome to Meteor” header. Nothing exciting, but we have our environment setup and it’s gonna be amazing from here :)

Data Layer with Collections: Model

Meteor is data-driven. I am convinced after years of Type Theory and Haskell research and development that by designing the right Types we can automate most of the mundane development for pretty much any business application, which makes development fast and maintenance a breeze. Meteor with typescript gets closer to this goal than many if not all others practically speaking.

It uses Mongo Collections for persistent data storage, and via the publish-subscribe mechanism and mini-mongo package on the client gives you practically the same API on both the server and the client. Let’s define the data layer for our Post and Comment types mentioned above. We will come to the User later on, as it is already provided by Meteor.

The best practice for data layer is the following: define a typescript interface, then a schema that corresponds to it, then a Meteor Collection based on these 2.

The above will save you so much trouble later on or compared to “pure” javascript, you can’t even imagine. Let’s start with Post data type. Create Post folder inside api folder (this is Meteor convention), create Post.ts file inside it and define our interface:

import { Mongo } from "meteor/mongo";

export interface IPost {
_id?: string,
title: string,
content: string,
createdAt: Date,
updatedAt: Date,
authorId: string
}

Everything here is pretty much self-explanatory. _id is the id that Mongo uses for every persistent document (we will abstract a lot of this away in future chapters), authorId is the reference to the User that created this post. We are using export keyword to be able to use this interface anywhere we need in our code. If you are using VS Code or similar IDE, typescript linters will help you tremendously in catching stupid errors via typing.

Once we have the interface defined, we can create a Collection to hold our Posts right away:

export const CollectionPosts = new Mongo.Collection<IPost>("posts");

Passing our interface to the constructor makes our life easier down the line — all collection methods will automagically know that they can only work with IPost interface types. The name “posts” will be the actual Mongo collection name in the database. We are exporting it to be able to use it anywhere else in our code — both on the server and the client. You will appreciate this shortly.

Now, Meteor Collections have an extensive api, but since we are trying to build an app using proper, scalable architectures with separation of concerns, let’s think about business logic from the start — as opposed to low-level Collection manipulation. We need to be able to create new posts and edit existing posts at the very minimum — let’s define these!

How would you manipulate the database in a “normal” web app? Usually, via some REST-ful api of varying degree of ugliness. Meteor is thankfully built on websocket protocol under the hood, which supports realtime two-way communication with the server and as such provides a much more elegant solution, namely

Meteor Methods: Controller

Meteor Methods is the mechanism to call server methods from the client and receive the result back via DDP protocol. DDP is the websocket-based protocol invented by Meteor, into details of which we will go into at a much later point, as from the high-level application developer point of view there’s no need to ever touch DDP directly.

We need to create the 2 methods — to add a new post and edit an existing post. Here is how it can be done, extremely straightforward:

Meteor.methods({
"posts.addNewPost"(p:IPost) {
p.createdAt = new Date()
p.updatedAt = p.createdAt
return CollectionPosts.insert(p);
},

"posts.editPost"(id:string, title:string, content:string) {
return CollectionPosts.update({_id:id},
{
$set: {
title: title,
content: content,
updatedAt: new Date()
}
})
}
})

The code above defines 2 methods, one of them inserts the new post into our collection, and the second one is given an existing post id, new title, and content, and then updates the post using standard mongo selectors and modifiers. We are also setting the created and updated dates on the server.

Once these methods are defined, you can call them both on the server and the client (!) using the same API:

Meteor.callAsync("posts.addNewPost", post)

// or, more extensively and without the callback hell of life before:
const res = await Meteor.callAsync("posts.addNewPost", post)

// if you need to catch errors either use try / catch, or chained catch(callback)

We are using callAsync because this will be the way going forward with Meteor 3.0. We will explain more how to handle errors etc using this new approach.

We will greatly improve upon this basic solution in a bit, but for now you can check that it works by making the following changes — add the following line to the server/main.ts :

import "/imports/api/Post/Post"

Start your app using meteor command again, navigate to localhost:3000 and then simply enter in the dev console something like:

const res = await Meteor.callAsync("posts.addNewPost", 
{title:"new post", content: {"hello world"})

You should receive an id of the newly created post in your res variable, and to make sure it’s been inserted you can check it in your local database. While your app is running, open another terminal window in your app folder and run meteor mongo command. You should get a mongo shell, and by running db.posts.find({}) you should see the newly inserted post. It works!

Schemas, Validation, and minimizing boilerplate

This is already much nicer than calling REST APIs, but we are missing some key parts of a robust application in such an approach. We want to be able to validate whatever is sent into our methods from the client at runtime, and we want an even nicer api instead of writing await Meteor.callAsync every time. Sure, we can write validation code explicitly in every method, but that’s too much unneeded boilerplate. What we need is a validation schema based on the interface IPost we already defined.

Since our goal is to be as practical as possible, we’ll use an excellent validation package simpl-schema. Install it into your project via the following command:

meteor npm install --save simpl-schema

Now we can define an SPost schema based on our IPost interface in our Post.ts file:

import SimpleSchema from "simpl-schema";

// ...

export const SPost = new SimpleSchema({
_id: {
type: String,
optional: true,
},
title: {
type: String,
},
content: {
type: String,
},
createdAt: {
type: Date,
},
updatedAt: {
type: Date,
},
authorId: {
type: String,
},
});

Now, this point is important. Our IPost interface already defines this schema. We are unfortunately forced to write the actual SimplSchema definition by hand to be able to use it at runtime. Down the line in this book, we will create a simple code generator that automates this task — boilerplate is bad, bad — but in the meantime, you can use ChatGPT with requests similar to the following:

create a simpl-schema definition based on the following interface:

interface IPost {
_id?: string,
title: string,
content: string,
createdAt: Date,
updatedAt: Date,
authorId: string
}

Produce code only, no explanation

Works like a charm, even with much more complex interfaces (with arrays, union types etc).

We will appreciate the full power of having a schema when we get to the UI part (validating forms is the most mundane and boring task you would probably face otherwise), but even on the server it is very useful — run a

SPost.validate(post)

and you’ll have a bunch of nicely formatted error messages if your post object does not correspond to your IPost interface at runtime. SimplSchema is very well documented and we will look at its different aspects along the way.

Now that we have an Interface, a corresponding Schema, a Collection, and a way to build methods, let’s create a nice and failure-proof business logic interface for our Posts!

ValidatedMethods: Better Business Logic Controller

Using Meteor Collection API both on the client and the server you can already create full-featured web apps, but they will be a bit error-prone and quite entangled. Since we want a scalable and easy-to-maintain MVC architecture with separation of concerns, let’s create a proper business logic / controller layer with the help of another handy package called ValidatedMethod, or more precisely mdg:validated-method . It is a meteor package, not an npm package — this is important. Its installation is no more difficult than npm though, just run the following command in your app folder: meteor add mdg:validated-method and that’s it.

ValidatedMethod is a handy wrapper that hides Meteor.methods and Meteor.call machinery inside, and adds flexible validation options on top. Let’s wrap our already defined two business logic methods — addNewPost and editPost — and make them a part of a “controller” object:

export const PostController = {

// add a new post
addNewPost: new ValidatedMethod({
name: "posts.addNewPost",
validate: SPost.validator(),
run(p:IPost) {
p.createdAt = new Date()
p.updatedAt = p.createdAt
return CollectionPosts.insert(p);
}
}),

// edit existing post
editPost: new ValidatedMethod({
name: "posts.editPost",
validate: null,
run(props: {id:string, title:string, content:string}) {
const {id, title, content} = props
return CollectionPosts.update({_id:id},
{
$set: {
title: title,
content: content,
updatedAt: new Date()
}
})
}
})
}

As you can see, it’s a straightforward change from the Meteor.methods call we defined earlier. The method name goes into name property of the ValidatedMethod, the actual code that we want to execute goes into run definition, and validate defines a validator function. We can write it by hand, but since we defined the schema for our IPost interface — SPost — turns out it already has a built-in validator function, which saves us a lot of effort.

To call these methods, we can simply run — again, both on the server and the client! — PostController.addNewPost(p) and Meteor does everything under the hood — runs the stub on the client, throws errors on the client, saves us a round-trip to the server in case of errors, and if everything is ok runs the code “properly” on the server and gives us back the result. We will look at actual usage for this once we get to the UI part.

For editPost code we do not provide a validator, since we are only calling it with id, title, and content, not all of the IPost fields. However, both of these methods are obviously very raw and lack very important functionality — users that own them. We need to define additional authentication checks so that only the logged-in user can create new posts and only the owner of the post should be able to edit it. Luckily, Meteor provides a very easy mechanism to do just that.

Let’s look at it, then define additional logic for the Comments entity, and then write a quick front-end using react and react-bootstrap.

Users, Authentication, Authorization

Please recall (again) that the goal of this book is to be as practical as possible. In that vein, we are not going to look at various different options to solve the user management and auth problems, but will gratefully use what Meteor gives us.

Namely, there is a built-in collection that handles users: Meteor.users

We will use it as a basis for our various advanced user management methods down the line, for now, it suffices to know it exists along with the methods to handle authentication via passwords and eventually using external systems such as Facebook or Google via OAuth.

To start using it, we’ll need to add another meteor package: meteor add accounts-password

This will give us the ability to register and login users, as well as a bunch of useful methods to handle authorization:

// id of the currently logged in user:
Meteor.userId()
// User collection object for the currently logged in user:
Meteor.user()

you can call them both on the server and on the client and they will return null if no user is logged in.

Since we are also using typescript and want to extend an already existing User object we can’t define a new interface for it, but have to extend the existing one. Create an imports/api/User folder and User.ts file inside of it and put the following code there:

declare module "meteor/meteor" {
namespace Meteor {
interface User {
/**
* Extending the User type
*/
firstName: string,
lastName: string
}
}
}

For now, we are simply adding first and last names for our users to the already existing fields, which can be seen on the following example object:

{
_id: 'QwkSmTCZiw5KDx3L6', // Meteor.userId()
username: 'cool_kid_13', // Unique name
emails: [
// Each email address can only belong to one user.
{ address: 'cool@example.com', verified: true },
{ address: 'another@different.com', verified: false }
],
createdAt: new Date('Wed Aug 21 2013 15:16:52 GMT-0700 (PDT)'),
profile: {
// The profile is writable by the user by default.
name: 'Joe Schmoe'
},
services: {
facebook: {
id: '709050', // Facebook ID
accessToken: 'AAACCgdX7G2...AbV9AZDZD'
},
resume: {
loginTokens: [
{ token: '97e8c205-c7e4-47c9-9bea-8e2ccc0694cd',
when: 1349761684048 }
]
}
}
}

There is a “writeable by default” profile property on the user object, but its usage is discouraged due to security reasons so let’s just ignore it — we will add all necessary fields at the top level.

In terms of business logic, we will need the methods to register new users and login existing users. For the latter, we will use the standard api, and for the former, we might be tempted to create a UserController similar to PostController defined above, but actually, in this case, it is better to also use Meteor standard api together with the onCreateUser callback to set the name properly. The reason for that is that Meteor standard user authentication methods encrypt the password automatically on the client for additional security — and we definitely want to take advantage of that.

Extend our User.ts file as follows:

import {Accounts} from 'meteor/accounts-base'

// ...

Accounts.onCreateUser((options, user) => {
const customizedUser = Object.assign({
firstName: options.firstName,
lastName: options.lastName
}, user);

return customizedUser;
})

Then on the client, we will simply call Accounts.createUser({email: "...", password: "...", firstName: "...", lastName: "..."}) and everything else will be taken care of. Of course, since the options object comes from the client, you may want to additionally check that firstName and lastName are reasonable strings and not full Wikipedia in json format.

Now that we know how to do some basic work with users, let’s adjust our Post methods to check user ownership correctly and define the Comments functionality to finalize our backend.

Final Controllers: Secure Post Business Logic

Go back to the Post.ts and let’s fix our controller methods:

addNewPost: new ValidatedMethod({
name: "posts.addNewPost",
validate: SPost.validator(),
run(p:IPost) {
const uid = Meteor.userId()
if (!uid) {
throw new Meteor.Error("not-authorized",
"only logged in users can create new posts")
}
p.createdAt = new Date()
p.updatedAt = p.createdAt
p.authorId = uid
return CollectionPosts.insert(p);
}
}),

We have added the check for the currently logged-in user (call to Meteor.userId() which always returns the currently logged-in user both on the server and on the client) and throwing an exception in case the user is not logged in. It’s a question of style whether to put such a check inside the validate method rather than an actual business logic call — we think it actually belongs inside validate, but for now for simplicity’s sake let’s keep it in the main run code. We are also setting up the authorId property to the correct userId on the server (since we do not trust the client).

For post editing, we’ll need some additional checks:

editPost: new ValidatedMethod({
name: "posts.editPost",
validate: null,
run(props: {id:string, title:string, content:string}) {
const {id, title, content} = props
// is the user logged in check?
const uid = Meteor.userId()
if (!uid) {
throw new Meteor.Error("not-authorized",
"only logged in users can create new posts")
}

// is the user owner of the post check?
const post = CollectionPosts.findOne({_id: id});
if (!post || (post?.authorId != uid)) {
throw new Meteor.Error("not-authorized",
"only owners may edit posts!")
}

return CollectionPosts.update({_id:id},
{
$set: {
title: title,
content: content,
updatedAt: new Date()
}
})
}
})

First, we are making the same “is the user logged in” check. Then, we are trying to get a post with an id passed from the client and checking if such a post exists and then if its owner is the same as the currently logged in user. If not, we are throwing another exception.

That’s it, we have the following business logic functions defined and they are enough for us to create a working blogging platform:

  • create new user
  • login user
  • create new post
  • edit post

Before we dig into creating UI with React and Bootstrap as promised, we are still missing one critical piece for our app — sure, we have these four business logic methods, but how do we actually view the existing posts on the platform? Should we create another method that searches for needed posts and sends them to the client by request?

Sure, we could do that, but Meteor has something better for this purpose — a reactive publish / subscribe mechanism. If we implemented this functionality via methods, we’d have to explicitly call such methods on the client each time we want to get an update — and this is ugly. Meteor publish / subscribe solves this issue by always pushing the latest and greatest data to the client, including any updates made elsewhere!

Let’s add this missing piece.

Publish and subscribe

Meteor pubsub mechanism allows you to use the same Mongo Collection api both on the server and on the client, while making sure the client always and reactively has exactly the data it needs. This is a very powerful and diverse mechanism and we will be looking at different advanced aspects of it further in the book, but for now, let’s start with the simplest case possible: let’s publish all posts to the client. This is obviously a bad solution for a large production app — so we will introduce paging in the next chapters, but we want to get to something working fast so let’s move in iterations.

Create a api/Post/publications.ts file and put the following code in it:

import { Meteor } from "meteor/meteor";
import { CollectionPosts } from "./Post";


Meteor.publish("posts.allPosts",
()=> CollectionPosts.find({}))

That’s it! Here we are defining a new “publication” called “posts.allPosts” which returns a reactive cursor with all posts from our collection. Then on the client, we can call Meteor.subscribe("posts.allPosts") and automagically we will have access to all posts via the same CollectionPosts object on the client! Amazing, isn’t it?

Again, as mentioned, this is the most basic way to use Meteor pub-sub, but we will delve much deeper into its power down the line.

Now, finally, let’s build our UI.

Unstyled React: Routes and Main Components

Let’s start with unstyled React to connect the front and back ends and get a fully-featured app on our hands. We will add Bootstrap and some nicer styling in the next section.

We will need the following pages / components on the front-end:

  • User Registration
  • User login
  • View all posts
  • Create new post
  • Edit existing post

We will use react-router v6 to handle routing in our app, so let’s add it right away: npm add --save react-router react-router-dom

Let’s also add Bootstrap and react-bootstrap:

npm add --save react-bootstrap bootstrap

We will look at how to integrate bootstrap styling with SCSS compilers later on, for now, we’ll just use unstyled react-bootstrap components and make them pretty in the next sessions. Yes, there are many different ways to create front-end for meteor apps, but React remains the most popular front-end javascript framework so let’s just roll with it.

Since it’s our first app to learn basic principles, we will be taking some sloppy shortcuts in designing our components to save time. Let’s start with Registration and Login. Go into imports/ui folder, create views subfolder there and create a LoginRegistration.tsx file inside.

Now, I personally absolutely hate writing React forms code. It’s tedious, mundane, boring, useless boilerplate. If you are ok doing it by hand — be my guest, but if you are like me — thankfully, we have ChatGPT that does this task perfectly. Use a prompt like this:

Write a typescript code for React dynamic form with validation 
and using react-bootstrap components that contains two fields:
login and password. Produce code only, no explanation.

It creates nicely formatted code for you, multiplying your productivity, with state tracking, validation, etc. Feel free to write better validation or make any other changes. In the next step, we will adjust this form to handle both registration and login and connect it to Meteor backend.

import React, { useState } from 'react';
import { Button, Form } from 'react-bootstrap';

type FormData = {
login: string;
password: string;
};

export const LoginRegistration: React.FC = () => {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<FormData>({ login: '', password: '' });

const validateForm = () => {
let formErrors = { login: '', password: '' };
if (!login) formErrors.login = "Login is required";
if (!password || password.length < 6) formErrors.password = "Password must have at least 6 characters";
setErrors(formErrors);

// return true if no errors
return Object.values(formErrors).every(x => !x);
};

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (validateForm()) {
console.log({ login, password });
}
};

return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formLogin">
<Form.Label>Login:</Form.Label>
<Form.Control
type="text"
value={login}
isInvalid={!!errors.login}
onChange={(e) => setLogin(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.login}
</Form.Control.Feedback>
</Form.Group>

<Form.Group controlId="formPassword">
<Form.Label>Password:</Form.Label>
<Form.Control
type="password"
value={password}
isInvalid={!!errors.password}
onChange={(e) => setPassword(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.password}
</Form.Control.Feedback>
</Form.Group>

<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}

Before we make these changes, let’s setup our basic routing to be able to work with UI. It’s a best practice to keep route definitions separately, but we are taking shortcuts so will define them in client/main.tsx file. Change its contents to the following:

import React from 'react';
import { createRoot } from 'react-dom/client';
import { Meteor } from 'meteor/meteor';

import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import LoginRegistration from '/imports/ui/Views/LoginRegistration';

const router = createBrowserRouter([
{
path: "login",
element: <LoginRegistration />,
}
])

Meteor.startup(() => {
const container = document.getElementById('react-target');
const root = createRoot(container!);

root.render(<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>);
});

We are defining a single route here that will display our LoginRegistration component to test that everything works. We are also adjusting the main rendering code slightly to make sure we support advanced routing in the future. Finally, adjust the server/main.ts file to import our User logic and publications from Post:

import { Meteor } from 'meteor/meteor';

import "/imports/api/Post/Post"
import "/imports/api/Post/publications"
import "/imports/api/User/User"

Now run meteor in your application folder (you can keep it running, it automatically reloads any changes you make). Navigate to localhost:3000/login and you should see something like this:

Login Form

Yay! Click “Submit” to check if the validation is working:

Login Form with Validation Errors

Nice. I mean, ugly, and we’ll fix it down the line — but it works! Now, let’s adjust this form so that it handles both Login and Registration functionality (shortcuts, remember — in the real app it has to be two different forms for obvious reasons).

Change the buttons for your form to these, while editing Form element to stop handling submit events:

<Form>

{/* ... */}

<Button variant="primary" onClick={()=>handleSubmit(true)}>
Register
</Button>
<Button variant="primary" onClick={()=>handleSubmit(false)}>
Login
</Button>

We will also adjust handleSubmit function to take a single boolean parameter that distinguishes between registration or login. Now let’s attach Meteor backend to it:

const handleSubmit = (registration: boolean) => {

if (validateForm()) {
//console.log({ login, password, registration });
if (registration) {
Accounts.createUser({
email: login,
username: login,
password: password
}, (err)=> {
console.log(err)
})
}
else {
Meteor.loginWithPassword(login,password,(err)=> {
console.log(err);
})
}
}
};

Very straightforward, if it’s a registration — we are calling createUser and if it’s a login, we are logging the user in.

Exercise: adjust the code to also ask the user for their name and last name and pass it to the server according to the code we wrote previously.

Let’s also add a welcome message for logged in users to make sure it works by adjusting react rendering code:

<>
{Meteor.userId() &&
<h3>Welcome, {Meteor.user()?.username}</h3>}
<Form>
{/* ... */}
</Form>
</>

Now register some user and then press login . You should see something like:

Login Form with Welcome Message

Great, it works!

Exercise: add nice error-handling messages to the react component.

Now, let’s give the ability to logged-in users to create blog posts (we will add editing functionality later on). We will need another react-bootstrap form for this, with two fields: title and content. Go and ask ChatGPT or write it yourself, and you should get something like the below, which you should put to the Views/EditPost.tsx file:

import React, { useState } from 'react';
import { Button, Form } from 'react-bootstrap';

type FormData = {
title: string;
content: string;
};

export const EditPost: React.FC = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [errors, setErrors] = useState<FormData>({ title: '', content: '' });

const validateForm = () => {
let formErrors = { title: '', content: '' };
if (!title) formErrors.title = "Title is required";
if (!content) formErrors.content = "Content is required";
setErrors(formErrors);

return Object.values(formErrors).every(x => !x);
};

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (validateForm()) {
console.log({ title, content });
}
};

return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formTitle">
<Form.Label>Title:</Form.Label>
<Form.Control
type="text"
value={title}
isInvalid={!!errors.title}
onChange={(e) => setTitle(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.title}
</Form.Control.Feedback>
</Form.Group>

<Form.Group controlId="formContent">
<Form.Label>Content:</Form.Label>
<Form.Control
as="textarea"
value={content}
isInvalid={!!errors.content}
onChange={(e) => setContent(e.target.value)}
/>
<Form.Control.Feedback type='invalid'>
{errors.content}
</Form.Control.Feedback>
</Form.Group>

<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}

Let’s also add a route for this to main.tsx :

import {LoginRegistration} from '/imports/ui/Views/LoginRegistration';
import { EditPost } from '/imports/ui/Views/EditPost';

const router = createBrowserRouter([
{
path: "login",
element: <LoginRegistration />,
},
{
path: "editpost",
element: <EditPost />,
}
])

Now if you navigate to localhost:3000/editpost you should see your Post form:

Post Form

Let’s now make sure it actually creates a new post by changing handleSubmit function to use our PostController api:

import { PostController } from '/imports/api/Post/Post';

// ...

const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (validateForm()) {
console.log({ title, content });
const pid = await PostController.addNewPost.call({
title: title,
content: content,
createdAt: new Date,
updatedAt: new Date,
authorId: Meteor.userId()
})
console.log("New post id:", pid)
}
};

Navigate to localhost:3000/editpost and create your first post:

First Post

You should see its id in the console. To be sure it’s been created, run meteor mongo in another terminal tab while meteor is running in your first one and enter db.posts.find() — you should see something like this:

Our Post in the db

Great, now let’s create a couple more posts and then make sure we can actually display them for readers in a separate component.

Final View: PubSub On The Client and Reactivity

Now that we have several posts in our database, let’s create the main page that will reactively show all posts. This section is important, as we will look at how to connect to Meteor publishes reactively while minimizing re-renders. For this, we will use another small and handy package — react-meteor-data. It should already be in your app since we created it with --typescript flag that also automatically installs React and key packages for working with it. So let’s proceed.

Create ui/MainPage.tsx file with the following code and let’s analyze it.

import React from 'react'
import { useSubscribe, useFind } from "meteor/react-meteor-data";
import { CollectionPosts, IPost } from '../api/Post/Post';
import Container from 'react-bootstrap/esm/Container';
import Row from 'react-bootstrap/esm/Row';
import Col from 'react-bootstrap/esm/Col';

export const MainPage = () => {

const loading = useSubscribe("posts.allPosts")
const posts = useFind(()=> CollectionPosts.find({}), [])

return (

<Container>
<Row>
<Col>
<h3>Welcome to our Blogs!</h3>
</Col>
</Row>
{posts.map((p:IPost,i:number)=> {
return (
<Row key={i}>
<Col>
<h4>{p.title}</h4>
<p className="text-muted">
{p.createdAt.toString()}
</p>
<p>
{p.content}
</p>
</Col>
</Row>
)
})}
</Container>
)
}

Also don’t forget to update the routing table in main.tsx

import { MainPage } from '/imports/ui/MainPage';

const router = createBrowserRouter([
{
path: "login",
element: <LoginRegistration />,
},
{
path: "editpost",
element: <EditPost />,
},
{
path: "/",
element: <MainPage />,
}
])

Now navigate to localhost:3000 and you should see all your posts there:

Post List

Now, check reactivity — open localhost:3000/editpost in another tab, add a post, and see that it appears automatically on your main page!

The magic lies in useSubscribe and useFind methods, which wrap some basic Meteor apis inside React hooks, making everything properly reactive. useSubscribe hides the call to Meteor.subscribe which connects us to the publish we defined above and returns a handler function that checks the subscription readiness — also reactive. This way, you can call loading() and return a nice loading screen while your data is being prepared — we will show how to do it nicely in the future chapters, but you can experiment now yourself.

useFind takes a function as an argument as well as array of arguments upon which the reactivity should depend. In our case it doesn’t depend on anything so we just pass an empty array. But again — you can appreciate the beauty that we use the same business logic controller both on the client and on the server:

const posts = useFind(()=> CollectionPosts.find({}), [])

The function we pass should return a cursor. Meteor makes sure internally all our finds are optimized and minimizes unnecessary rerenders.

Pubsub is a very powerful mechanism that allows fine control and we will look at its advanced features in future chapters. For now, this is just to get you hooked on how easy it is to create proper MVC architectures with Meteor and React. The code is concise, elegant, and easy to maintain — as usual, the most mundane part is actually the front-end, but here thankfully ChatGPT can help us a lot.

In the next chapter, we will show you how to apply styling using Bootstrap and SCSS compilers — some of these settings are not straightforward and we hope our guidelines will save you a couple of hours of googling.

Stay tuned and appreciate your comments, questions, improvements!

--

--

Anton Antich
Superstring Theory

How to scale startups and do AI and functional programming. Building Integrail.ai: pragmatic AGI platform. Built Veeam from 0 to 1B in revenue in under 10 years