Building Feature-rich Apps with Zero Backend Code
Tags: serverless, webdev, javascript, typescript
Published: 2020-07-09 10:58:04

The availability of various cloud services makes developing apps ever easier. Now it's possible to develop sophisticated apps without writing any backend code. Instead of becoming a "full-stack" engineer, you can focus on the frontend - what the user sees and interacts with.

This article introduces how to build a diary web app without any backend code. A user can create an account and write diary entries only she can view. There is a [demo deployed at Netlify](https://serverless-diary.netlify.app) in case you want to play with it first. The complete project is at [GitHub](https://github.com/hjiang/diary-demo).

We will use React for the frontend and [LeanCloud](https://leancloud.app) to store data. Create a React app and install the LeanCloud JavaScript SDK:

```shell-session
npx create-react-app diary-demo --template typescript
cd diary-demo
npm i -S leancloud-storage
```

The generated project includes TypeScript support. I've formed the habit of using typescript for all frontend projects.

To follow along, you need to create a free [LeanCloud](https://leancloud.app) account, create an app, and copy the app Id and app Key from the app settings page. I usually put the initialization code in a file called `lc.ts` and import it in other files.

```typescript
import LC from 'leancloud-storage';

LC.init({
  appId: 'YOUR_APP_ID',
  appKey: 'YOUR_APP_KEY'
});

export default LC;
```

If you've used any third-party services before, you probably are wondering: isn't placing the app key in frontend code insecure? Bear with me. I'll address the security issue soon.

There is no shortage of React tutorials, so I'm only going to discuss code that replaces what you would otherwise accomplish with your own backend API.

## Registration and Login

First, we need to let users create accounts and login.

LeanCloud provides a feature-rich user account system, but for the sake of simplicity, we will not deal with email/SMS verification and third-party logins. We will simply ask a new user to create a username and a password. The complete sign-up page is [here](https://github.com/hjiang/diary-demo/blob/master/src/SignupPage.tsx). Besides the UI part, there're only a few interesting lines:

```typescript
    const user = new LC.User();
    user.setUsername(username);
    user.setPassword(password);
    try {
      await user.signUp();
      setShowSuccessMsg(true);
    } catch (e) {
      setError(e.message);
    }
```

We simply create a new `User` object, set the `username` and `password` attributes, and create the new account. `user.signUp()` resolves to the new account created in the cloud, but we are ignoring the result here.

Login ([complete code](https://github.com/hjiang/diary-demo/blob/master/src/LoginPage.tsx)) is even simpler:

```typescript
    try {
      await LC.User.logIn(username, password);
      history.push('/diary');
    } catch (e) {
      setError(e.message);
    }
```

After a successful login, we redirect the user to the `/diary` page. The current authenticated user can be obtained from `LC.User.current()`.

## Creating and reading diary entries

Our main application logic is on the diary page ([complete code](https://github.com/hjiang/diary-demo/blob/master/src/DiaryPage.tsx)). There is only one type of data - diary entries. Let's name it `Entry`. In LeanCloud terminology each type of data is called a *class*. You can consider it a table in a database. Creating a Class in code is simple:

```typescript
const Entry = LC.Object.extend('Entry');
```

Though the class is not actually created in the cloud until the first object of this type is saved. Saving a new object is similar to the registration code we've seen before:

```typescript
    const entry = new Entry();
    try {
      const savedEntry = await entry.save({
        user: LC.User.current(),
        content: newEntry
      });
      setEntries([
        {
          id: savedEntry.id!,
          content: savedEntry.get('content'),
          date: savedEntry.createdAt!
        },
        ...entries
      ]);
    } catch (e) {
      setError(e.message);
    }
```

Note that we store the current user in the `user` attribute, so that later we can retrieve the entries belonging to this user. After saving the new entry, we prepend it to the list of entries which should be populated when the page is loaded.

To populate the entries, we use the React `useEffect()` hook to fetch all entries belonging to the current user sorted by creation time in descending order:

```typescript
  const [entries, setEntries] = useState<DiaryEntry[]>([]);
  const me = LC.User.current();

  useEffect(() => {
    const fetchEntries = async () => {
      const query = new LC.Query('Entry');
      query.equalTo('user', LC.User.current());
      query.descending('createdAt');
      try {
        const fetchedEntries = await query.find();
        setEntries(
          fetchedEntries.map(entry => {
            return {
              id: entry.id!,
              content: entry.get('content'),
              date: entry.createdAt!
            };
          })
        );
      } catch (e) {
        setError(e.message);
      }
    };
    fetchEntries();
  }, [me]);
```

Now, users can sign up and login, post and read entries. We've implemented all the basic features. But the job is not done, and we must return to the security concern raised earlier.

## Security

As we mentioned earlier, the API key is exposed in the frontend code. Even if we minimize and obfuscate the code, it is trivial for someone to find the key by looking at network requests. A malicious user can forge requests to read or overwrite other users' data. The mechanism to safeguard data is access-control list (ACL). When creating a piece of data, we need to consider who should have access to it and save the permission with the data. For example, in our case no one should have access to an entry except its author, so we should add the following lines before calling `entry.save()`:

```typescript
    const acl = new LC.ACL();
    acl.setPublicReadAccess(false);
    acl.setPublicWriteAccess(false);
    acl.setReadAccess(me, true);
    acl.setWriteAccess(me, true);
    entry.setACL(acl);
```

Now each user can only access her own entries after login.

This concludes the article. I plan to follow up with more articles about implementing full-text search and realtime updates. Please feel free to leave a comment if you have any questions or want to know more!