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 in case you want to play with it first. The complete project is at GitHub.
We will use React for the frontend and LeanCloud to store data. Create a React app and install the LeanCloud JavaScript SDK:
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 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.
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. Besides the UI part, there're only a few interesting lines:
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) is even simpler:
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). 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:
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:
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:
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()
:
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!
Top comments (6)
Let's do form validations just on frontend!!
Good point. There is a LeanCloud-specific solution called beforeSave hooks where you can check an object before it's saved. It is backend code but still much less work than writing and deploying an API server.
Are you really focusing on frontend with this login form?
This article wasn't intended to be about frontend techniques, but to show how all the application logic can be placed in the frontend. So I intentionally kept the code simple (and the UI ugly).
You are right, I appreciate it, but the first impression when click at the demo link was not good so doesn't complete the article.
Thanks for the feedback. You're right that first impression can affect motivation. I'll spend some time to make it look nicer.