DEV Community

Cover image for Using Mountaineer to develop a React app with Python
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Using Mountaineer to develop a React app with Python

Written by Rosario De Chiara✏️

Mountaineer is a framework for building web apps in Python and React easily. The idea of Mountaineer is to let the developer leverage previous knowledge of Python and TypeScript to use each language (and framework) for the task they are most suitable for.

The basic idea is to develop the frontend as a proper React application and the backend as several Python services: everything in a single project, types consistent up and down the stack, simplified data binding and function calling, and server rendering for better accessibility.

In this article, we will describe the setup of a Mountaineer project and show how to develop a simple application containing all the basic concepts. By the end, you'll be able to seamlessly integration frontend and backend components in a single project.

Setting up the Mountaineer development environment

Before starting, it is important to set up a proper environment. Mountaineer essentially transplants a React project, which is a Node application, into a Python environment. So, it can be quite picky about versions of each component in the environment.

In my setup, WSL is running quite an old version of Ubuntu (Ubuntu 20.04.6 LTS) under Windows, so I had to update the following packages:

  • Rust language: Updated to version greater or equal to 1.77
  • Go language: Updated to version 1.22
  • Node: Updated to version greater or equal to 20
  • Python language: Updated to version greater or equal to 3.11

These settings will change once Mountaineer becomes stable. It is also worth mentioning that the author was responsive and supportive when I had problems setting up the system.

Once your environment is set, you can create a boilerplate application by using the following command:

pipx run create-mountaineer-app
Enter fullscreen mode Exit fullscreen mode

This is the facility available to create the (pretty intricate) directory structure with all the dependencies of the two, co-existing environments: the Python project and the Node project.

In the following figure, you can see the output of the command, which may vary depending on pre-existing requisites:

$ pipx run create-mountaineer-app
? Project name [my-project]: microblog
? Author [Rosario De Chiara <rosdec@gmail.com>]
? Use poetry for dependency management? [Yes] Yes
? Create stub MVC files? [Yes] No
? Use Tailwind CSS? [Yes] No
? Add editor configuration? [vscode] vscode

Creating project...
Creating .gitignore
Creating docker-compose.yml
Creating README.md
Creating pyproject.toml
Creating .env
Creating microblog/app.py
Creating microblog/main.py
Creating microblog/cli.py
Creating microblog/config.py
Creating microblog/__init__.py
Creating microblog/views/package.json
No content detected in microblog/views/tailwind.config.js, skipping...
No content detected in microblog/views/postcss.config.js, skipping...
No content detected in microblog/views/__init__.py, skipping...
No content detected in microblog/views/app/main.css, skipping...
No content detected in microblog/views/app/home/page.tsx, skipping...
No content detected in microblog/views/app/detail/page.tsx, skipping...
No content detected in microblog/controllers/home.py, skipping...
No content detected in microblog/controllers/detail.py, skipping...
Creating microblog/controllers/__init__.py
No content detected in microblog/models/detail.py, skipping...
Creating microblog/models/__init__.py
Project created at /home/user/microblog
Creating virtualenv microblog-IiOnN0qh-py3.11 in /home/user/.cache/pypoetry/virtualenvs
Updating dependencies
Resolving dependencies... (1.3s)

Package operations: 29 installs, 0 updates, 0 removals

  [....INSTALLATION OF PYTHON PACKAGES....]

Writing lock file

Installing the current project: microblog (0.1.0)
Poetry venv created: /home/user/.cache/pypoetry/virtualenvs/microblog-IiOnN0qh-py3.11

added 129 packages, and audited 130 packages in 9s

34 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Environment created successfully
Creating .vscode/settings.json
Editor config created at /home/user/microblog
$
Enter fullscreen mode Exit fullscreen mode

If you look closely, you can spot two distinguishable phases of dependencies installation. Note how create-mountaineer-app is not only able to generate the scaffolding of the app but also to populate it with a sample MVC project.

For this article, we won’t use this function but we will implement a sample application by hand with this command:

  $ poetry run runserver
Enter fullscreen mode Exit fullscreen mode

You will be able to start the web server (see http://127.0.0.1:5006/) and begin developing the application with the useful hot reload once the source code is modified.

I must admit, the first run is not very exciting – there is no loaded page and the output is pretty dull:

Plain Application

Before starting the development, let’s look at the directory structure that is created by the script we just ran. From the descriptions, you should be able to understand where to concentrate depending on what you intend to do:

Directory Structure

Adding the controller and the view

Creating a page involves setting up a new controller to define the data that can be pushed and pulled to your frontend.

Let’s create a controller named home.py in the directory controller; as a controller, it will run on the backend and, as you probably expect, it is a Python script.

The general idea is to have an instance of the ControllerBase class and implement the method render that provides the raw data payload that will be sent to the frontend on the initial render and during any side effect update.

In most cases, you should return a RenderBase instance (see below) but if you have no data to display, you can also return None, like we do here. Additionally, we have to set the url parameter that contains the URL on which the controller is reachable, and the view_path that associates the view to this controller.

The render() function is a core building block of Mountaineer and all controllers need to have one: it defines all the data that your frontend will need to resolve its view:

from mountaineer import ControllerBase

class HomeController(ControllerBase):
    url = "/"
    view_path = "/app/home/page.tsx"

    async def render( self ) -> None:
        pass
Enter fullscreen mode Exit fullscreen mode

The next step is to register the new controller in app.py:

from mountaineer.app import AppController
from mountaineer.js_compiler.postcss import PostCSSBundler
from mountaineer.render import LinkAttribute, Metadata

from intro_to_mountaineer.config import AppConfig
from intro_to_mountaineer.controllers.home import HomeController

controller = AppController(
    config=AppConfig(),  # type: ignore
)

controller.register(HomeController())
Enter fullscreen mode Exit fullscreen mode

At this point, if you save your file, the server will automatically reload, registering the new controller but complaining about the missing page.tsx, which is the view we have associated with the new controller.

The view is, of course, a TypeScript/React file:

import React from "react";
import { useServer } from "./_server/useServer";

const Home = () => {
  const serverState = useServer();

  return (
    <div>
      <h1>Home</h1>
      <p>Hello, world!</p>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

You have to create the directory path /app/home/ under /view/ where our React project lives and place the file above, named page.tsx.

If you placed everything in the correct places, your browser will refresh and show the newly created page:

Hello World Webpage

If you have a problem placing the right file in the directory, check this specific commit on the repo and see how the application looks.

Adding the model

One smart idea of Mountaineer is that it comes with the support of a PostgresSQL database to back your data. To facilitate its use, together with the files in your project, the create_mountaineer_app script also creates a docker-compose.yml YAML file to start the PostgresSQL server in your docker. You do not have to handle the process of building tables; Mountaineer will just inspect your code in the /model directory to understand what objects you need in the database.

The first step is to create a new Python script. In this example, it will be blogpost.py in the /model directory:

from mountaineer.database import SQLModel, Field
from uuid import UUID, uuid4
from datetime import datetime

class BlogPost(SQLModel, table=True):
    id: UUID = Field(default_factory=uuid4, primary_key=True)

    text: str
    data: str = datetime.now()
Enter fullscreen mode Exit fullscreen mode

To add this file to the project, you have to include it in the __init__.py Python file:

from .blogpost import BlogPost
Enter fullscreen mode Exit fullscreen mode

At this point, we are ready to create the table in the database with the following commands. Of course, you must have a docker installation in your system on which to instantiate the container:

  docker-compose up -d
  poetry run createdb
Enter fullscreen mode Exit fullscreen mode

The createdb script will look into the /model directory and, for each object that has been included in the initialization file __init__.py, it will create a table with columns that match the names and types defined in the blogpost.py file (for this example).

Out of curiosity, if you check your PostgresSQL database, you can see the structure of the blogpost table:

Blogpost Table Structure

Now we have all the elements in place to complete the development of the application. First, let’s add the functionality for adding a new blog post. To do so, we have to add the input fields to the frontend (in the /views directory) and update the controller to properly add a row to the blogpost table (in the /controllers directory).

We first add a new method in the backend, which is the controller, in the file /controllers/home.py (the complete file is on the repo):

class HomeController(ControllerBase):
    url = "/"
    view_path = "/app/home/page.tsx"

    async def render(self) -> None:
        pass

    @sideeffect
    async def add_blogpost( self,
        payload: str,
        session: AsyncSession = Depends(DatabaseDependencies.get_db_session)
    ) -> None:
        new_blogpost = BlogPost(text=payload)
        session.add(new_blogpost)
        await session.commit()
Enter fullscreen mode Exit fullscreen mode

In the code above, we define the method add_blogpost that is marked by the @sideeffect decorator. This says to Mountaineer that this code will have an impact on the data and for this reason, when invoked, the server state must be reloaded. The add_blogpost method will simply input the parameter payload, which will contain all the data passed through from the backend and the session that contains the pointer to the database. By using the payload, we create a new BlogPost object instance, using the payload string to initialize it. The new_blogpost object is added to the session, which is translated to an insert in the Blogpost table in the database.

The second modification is in the view. Below is a snippet from the page.tsx file in the /views/app/home (the complete file is on the repo):

const CreatePost = ({ serverState }: { serverState: ServerState }) => {
  const [newBlogpost, setNewBlogpost] = useState("");

  return (
    <div>
      <input type="text"
        value={newBlogpost}
        onChange={(e) => setNewBlogpost(e.target.value)} />
      <button
        onClick={
          async () => {
            await serverState.add_blogpost({
                payload: newBlogpost.toString()
            });
            setNewBlogpost("");
          }}>

        Post</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Working on the frontend, we define a new React component that is just a simple form to handle the textbox and the onClick event when we submit it. As you can see in the code above, we just invoked the backend service add_blogpost defined in the controller.

In the following image, you can see the real superpower of Mountaineer: the strong interconnection between the Python project and the React/TypeScript project:

Codeblock Example For Python Project And The React/TypeScript Project

In the React/TypeScript part of the codeblock above, we see the new function, add_blogpost() , added to the controller without any further configuration. Each time we modify the source code, Mountaineer incorporates the modifications by updating the type hints and function suggestions.

Once you complete all the modifications in the browser, you will be able to see the new UI:

New Blog UI

When you interact with the form by checking the database, you will be able to see that the whole process works:

Data Flow From The Frontend To Database

Now we have a proper data flow from the frontend to the database through the backend. The last step is to implement the flow in the opposite direction, from the database to the frontend. This will involve changes in two places: the controller and the view.

The controller will have a new version of the render function:

    async def render(
        self,
        request: Request,
        session: AsyncSession = Depends(DatabaseDependencies.get_db_session)
    ) -> HomeRender:
        posts = await session.execute(select(BlogPost))

        return HomeRender(
            posts=posts.scalars().all()
        )
Enter fullscreen mode Exit fullscreen mode

render is invoked during the initial render and on any sideeffect update. Its purpose is to provide the data from the database that will be sent to the frontend. The updates go into the server state by using the RenderBase instance to update it. In the example above, you can see how the array named posts is filled by executing a query on the specific object BlogPost.

On the view, we just have to manipulate the data from the serverState and assemble them in the interface: for this purpose, we wrote a new React component named ShowPosts:

const ShowPosts = ({ serverState }: { serverState: ServerState }) => {
  return (
    serverState.posts.map((post) => (
      <div key={post.id}>
        <div>{post.text}</div>
        <div>{post.data}</div>
        <br></br>
      </div>
    )))
}
Enter fullscreen mode Exit fullscreen mode

We already know that in the serverState object, we will find an object named posts that is populated in the controller above. The updated files are on the repo and the final result is the following:

Updated Blog UI

Conclusion

This project has some similarities with projects like ReactPy and PyReact. However, unlike those projects, it allows you to integrate both the frontend written in pure TypSscript/React and the backend in pure Python/Uvicorn into a single condebase, achieving strong coupling between the two layers by using Mountaineer.


Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)