Build a Full-stack React app with Zero Configuration
Building a modern frontend application typically requires a lot of tooling. Think Babel, Webpack, Parcel, Rollup etc. There’s a reason module bundlers are so popular.
There are lots of great tools to help simplify the process of beginning a new frontend project. If you’re not new to React, or haven’t been living under a rock then you must have used create-react-app
. It’s easy and convenient. Opinionated, yes. But it takes away a lot of the painful setup you may have had to do on your own.
So, what do I mean by Zero configuration?
In this article, I’ll walk you through building a full-stack React app with NodeJS on the backend, and we will do this without writing any configurations! No web pack, no complex setups. None. Nada, zilch.
The tool that avails us this ease is called Zero. Zero aka Zero Server, prides itself as a zero configuration web framework.
I think it's a decent framework with potential. It definitely saves you a lot of stress and is capable of handling very different project setups. Your backend could be in python or Node! Your frontend could be in Vue, React or Svelte!
As with everything, there are a few gotchas with Zero. Some major, some minor, depending on your use case. I’ll make sure to highlight these in the article as we build the project.
The Full-stack Application
We will be building an application for the fictionally famous Angela McReynolds. Have a look at the deployed version of the application to know all about her. Have a click around!
The major bits of the application include a homepage built in React:
And a list of past projects for potential clients to have a look at:
Installation and Getting Started with Zero
Writing the first line of code and getting something on the screen is as easy as it gets with Zero.
Create a new folder wherever on your computer and have that opened in your code editor.
Within this directory, create a new file index.tsx
. This is going to be the entry point of the app. The homepage. I’ll explain how that works shortly.
Go ahead and write a basic App
component as show below:
import React from "react";
const App = () => {
return <h1>Hello!</h1>;
};
export default App;
This just renders the text Hello!
. Nothing fancy — yet.
We haven’t installed react
or any module at this point. That’s fine. Zero
will take care of that. In your terminal, go ahead and write the following to run zero against the directory.
npx zero
:
What happens after running the zero
command is interesting. It goes ahead and resolves the modules in index.tsx
, installs the dependencies and automatically creates configuration files, so you don’t have to!
Now go to http://localhost:3000
and you should have the index.tsx
component served:
This is not particularly exciting but there’s something to learn here still!
NB:
We ran the zero
server command without a global installation. This is possible because we used npx. I’m going to favour this throughout the article. If you’d rather have zero
installed globally run npm install -g zero
and start the application by just running zero
NOT npx zero
.
How Routing in Zero-server works
Zero uses a file-based routing system. If you’ve worked with certain static site generators, then this may not be new to you. This is also a system embraced by NextJS.
The way it works is simple. The current zero server application is running on http://localhost:3000/
. The page served to the browser will be the root index
file. This could be index.html
or index.jsx
or index.tsx
— it doesn’t matter. Zero would still compile and serve the file.
If you visited /about
in the browser, zero would look for an associating about
file in the root directory. Regardless of the file type, as long as it's a supported file type, i.e., .vue
, .js
, .ts
, .svelte
, or .mdx
.
If you visit /pages/about
, then zero would look for an about
file in the pages
directory.
Simple, yet effective routing.
Here’s a practical example.
Create a new file about.tsx
in the root directory and have the following basic component returned:
import React from "react";
const About = () => {
return <h1>About me!</h1>;
};
export default About;
And sure enough if you visit http://localhost:3000/about
you’ll see the following:
NB: Zero will look for the default exported entity in the file being rendered. Make sure to have a default export NOT named exports in your public React files.
How about subdirectories?
Create a blog
directory and in it create a hello.mdx
file.
And write the following:
# Hello there
## This is a new blog
This is just markdown! But still Zero
renders that just fine!
You’d notice that the file extension reads .mdx
. This is a superset of markdown .md
. In elementary terms, mdx
is markdown plus JSX. Picture being able to render a React component in a markdown file! Yes, that’s what MDX lets you do.
Folder Structure for your Zero App
Since routing is file-based, some more thought need to be put into how you structure your app.
While developing, you wouldn’t want all your client files publicly exposed. Some components will exist just to be composed into pages and not to be displayed by themselves.
My recommendation: place files you want public in the main directory (2) and everything else should go in a client
directory (1)
What you name this directory is up to you. You could call it components
if you wish. But make sure to have this separation of concerns in your zero app.
You’ll see why this is gold in a bit.
Ignoring files with a .zeroignore file
Files or directories you don’t want public can be communicated to zero via a .zeroignore
file.
Like a gitignore
file, you write the name of the directory or file to be ignored. In this example, here’s what the .zeroignore
file looks like:
client
server
Ignoring the client
and server
directories. The client directory will hold client-side files we don’t want public, same goes for server
.
Building the HomePage
Right now we’ve got a homepage that just says “Hello”. No one’s ever going to be impressed with that! Let’s improve it.
Since this blog is focused on working with zero server, I won’t be explaining the UI stylistic changes made. For rapid prototyping, Install chakra and styled-components:
npx yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion styled-components
Now update the index.tsx
file to the following:
import React from "react";
import {
Flex,
Box,
Heading,
Text,
Button,
Center,
Image,
Spacer,
} from "@chakra-ui/react";
const Home = () => {
return (
<Flex direction={["column", "column", "row", "row"]}>
{/* Profile Card */}
<Box
flex="1.5"
p={[10, 10, 20, 20]}
minH={["auto", "auto", "100vh", "100vh"]}
bg="linear-gradient(180.1deg, #CCD0E7 69.99%, rgba(144, 148, 180, 0.73) 99.96%)"
>
<Center height="100%">
<Box w="70%" maxW={650} minW={400} minH={400}>
<Flex justify="center">
<Box
borderRadius={10}
bg="rgba(209, 213, 230, 0.5)"
w="70%"
maxW={400}
height={200}
>
<Flex
direction="column"
align="center"
justify="center"
height="100%"
>
<Image
borderRadius="full"
boxSize="100px"
src="https://i.imgur.com/95knkS8.png"
alt="My Avatar"
/>
<Text textStyle="p" color="black">
Angela McReynolds
</Text>
</Flex>
<Flex mt={4} color="rgba(110, 118, 158, 0.6)">
<Button
borderRadius={6}
py={6}
px={8}
bg="linear-gradient(96.91deg, rgba(255, 255, 255, 0.44) 5.3%, #BDC3DD 83.22%)"
>
Read my blog
</Button>
<Spacer />
<Button
borderRadius={6}
py={6}
px={8}
bg="linear-gradient(96.91deg, rgba(255, 255, 255, 0.44) 5.3%, #BDC3DD 83.22%)"
>
About me{" "}
</Button>
</Flex>
<Box mt={6}>
<Text
textStyle="p"
textAlign="center"
color="black"
opacity={0.1}
>
© 2020 Angela McReynolds
</Text>
</Box>
</Box>
</Flex>
</Box>
</Center>
</Box>
{/* Details */}
<Box flex="1" bg="black" p={[10, 10, 20, 20]}>
<Heading as="h1" color="white" textStyle="h1">
THE <br />
WORLD'S BEST
<br /> FRONTEND
<br /> ENGINEER
</Heading>
<Text textStyle="p">
Forget about hype, self affirmation and other bullshit. I don’t do
those.
</Text>
<Text textStyle="p">
I’ve got results. in 2015, 2016, 2017, 2018 and 2020 I was voted the
world’s best frontend engineer by peers and designers all around the
world.
</Text>
<Text textStyle="p">
A thorough election was conducted, and I came out on top. I’ve got
brains and I use them, You’re lucky to have stumbled here.
</Text>
<Text textStyle="p">
While living on Mars i spent decades mastering the art of computer
programming. On arriving earth in 2013, I constantly laughed at our
pathetic the developers on earth were. You're all lucky to have me.
</Text>
<Box>
<Button
bg="linear-gradient(96.91deg, #BDC3DD 5.3%, #000000 83.22%)"
w={"100%"}
color="white"
_hover={{ color: "black", bg: "white" }}
>
See past projects
</Button>
</Box>
</Box>
</Flex>
);
};
export default Home;
Now you should have something like this when you visit localhost:3000
:
Global Centralised Page Configurations
This right here is one of the biggest downsides of Zero. Out of the box, there’s no way to handle centralised page configurations. You’ve got to be creative. In many cases, you can figure this out, while others may turn out hacky.
In this particular scenario, we want to add centralised settings for the chakra UI library.
You’ll have cases like this in your app, so here’s what I recommend.
Start off by populating the client
directory with some structure that lets you house each page independent of the publicly exposed file.
Don’t get confused. Here’s what I mean. Create a pages
directory and have Home
and About
subdirectories created. Move over the code from the public index.tsx
and About.tsx
into the respective directories.
In this example, I have all the code for Home
moved over like this:
// Home/Home.tsx
export const Home = () => {
// copy code over
}
// Home/index.ts
export {Home as HomePage} from './Home'
Go ahead and do the same for the About
page and export both from pages/index.tsx
:
export { AboutPage } from "./About";
export { HomePage } from "./Home";
Now, here comes the good part.
Centralise whatever central page creation logic you’ve got in a separate file within the client directory. I’ve called this makePages.tsx
Theming, metadata, custom font … all of that added in one place. Here’s what we need for the example app:
Install react-helmet-async:
npx yarn add react-helmet-async
Then add the following to makePages.tsx
:
import React from "react";
import { Helmet } from "react-helmet-async";
import { ChakraProvider, Box } from "@chakra-ui/react";
import { extendTheme } from "@chakra-ui/react";
const appTheme = extendTheme({
colors: {
brand: {
100: "#CCD0E7",
200: "6E769E60",
800: "BDC3DD",
900: "#9094B4",
},
},
fonts: {
heading: `"Roboto Condensed", sans-serif`,
body: "Roboto, sans-serif",
mono: "Menlo, monospace",
},
textStyles: {
h1: {
fontSize: ["4xl", "5xl"],
fontWeight: "bold",
lineHeight: "56px",
},
p: {
fontWeight: "bold",
py: 4,
color: "rgba(204, 208, 231, 0.5)",
},
},
});
type PageWrapperProps = {
children: React.ReactNode;
title: string;
};
export const PageWrapper = ({
children,
title,
}: PageWrapperProps & React.ReactNode) => {
return (
<>
<Helmet>
<meta charset="UTF-8" />
<title>{title}</title>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@700&family=Roboto:wght@700&display=swap"
rel="stylesheet"
/>
</Helmet>
<ChakraProvider theme={appTheme}>
<Box w="100%" h="100vh">
{children}
</Box>
</ChakraProvider>
</>
);
};
Now, your app will be different, but you will still benefit from the structure described here. And perhaps save yourself a lot of time debugging and duplicating code.
Now, we need to use the PageWrapper
component from makePages.tsx
. PageWrapper
takes a page component and ensures it's got all the centralised logic.
Go to Home/index.tsx
and have it use PageWrapper
as seen below:
// client/pages/Home/index.tsx
import { PageWrapper } from "../../makePages";
import { Home } from "./Home";
export const HomePage = () => (
<PageWrapper title="Home">
<Home />
</PageWrapper>
);
Do the same for the About
page by following the pattern established above.
In the exposed index.tsx
home page, i.e. the root file served for localhost:3000
, go ahead and use the new HomePage
component:
// index.tsx
import { HomePage } from "./client/pages";
export default () => <HomePage />;
This now has all the centralised configuration for our client pages. Here’s the result:
Everything’s coming along nicely!
Go ahead and build yourself and About
page. Use the same structure as above and see how that works too!
Customising the 404 Pages
If you go ahead and visit a random page like http://localhost:3000/efdfdweg
you should see the following:
That’s okay.
This is the default 404 page from zero. To customise this, you just have to create a 404
file (in any supported file ending) and zero
will have that served.
Let’s try that.
Create a 404.jsx
file and have the following copied over:
// 404.jsx
import React from "react";
import {
Container,
Heading,
Text,
Link,
Center,
Image,
} from "@chakra-ui/react";
import { PageWrapper } from "./client/makePages";
export default () => (
<PageWrapper>
<Container bg="black">
<Heading textStyle="h1" mt={7} textAlign="center" color="white">
You seem lost :({" "}
</Heading>
<Text textStyle="p" textAlign="center">
<Link href="/" color="brand.900">
Go home
</Link>
</Text>
<Image src="https://i.imgur.com/lA3vpFh.png" />
</Container>
</PageWrapper>
);
And sure enough, we’ve got an arguably nicer 404 page. Could be funny or inspiring, depending on who is reading.
Server-side development with Zero
We’ve got the essential functionality you need to be aware of on the client covered. This includes tricky bits such as centralising your page set up.
Let’s switch focus to the backend for a bit. I’ll be using NodeJS to keep things familiar.
Before any code implementation, you should be aware that routing works just the same here! And as with the client implementation, zero supports different backend languages: python and node.
Okay, so first things first.
When a user clicks See past projects
, we want to display a new page with a list of projects served from our backend written in zero.
Let’s set up a basic NodeJS backend.
Create an api
directory, and a projects.ts
file. All endpoints will be written in this api
directory. Essentially, the endpoint will be something like ${APP_BASE_URL}/api/projects
— which is semantic!
Since we’re using typescript, install the typing for node as follows:
npx yarn add @types/node -D
Now paste the following in the projects.ts
file:
const PROJECTS = [
{
id: 1,
client: "TESLA",
description: "Project Lunar: Sending the first humans to Mars",
duration: 3435,
},
{
id: 2,
client: "EU 2020",
description:
"Deploy COVID tracking mobile and TV applications for all of Europe",
duration: 455,
},
{
id: 3,
client: "Tiktok",
description:
"Prevent US app ban and diffuse security threat claims by hacking the white house",
duration: 441,
},
];
module.exports = function (req, res) {
res.send({ data: PROJECTS });
};
This is a basic implementation, but note the express style syntax:
module.exports = function (req, res) {
res.send({ data: PROJECTS });
};
Where req
and res
represent the request and response objects. If you visit localhost:3000/api/projects
you should now receive the JSON object.
Now all we’ve got to do is make the fronted call this API endpoint.
In the pages
directory, add a new Projects
folder.
Go ahead and paste the following in projects.tsx
within this folder. Don’t worry I’ll explain the important bits.
import {
Thead,
Tbody,
Table,
Tr,
Th,
Td,
Heading,
TableCaption,
Box,
} from "@chakra-ui/react";
import { useState, useEffect } from "react";
export const Projects = () => {
const [projects, setProjects] = useState([]);
useEffect(() => {
const fetchData = async () =>
await fetch("/api/projects")
.then((res) => res.json())
.then(({ data }) => setProjects(data));
fetchData();
}, []);
return (
<Box
flex="1.5"
p={[10, 10, 20, 20]}
minH="100vh"
bg="linear-gradient(180.1deg, #CCD0E7 69.99%, rgba(144, 148, 180, 0.73) 99.96%)"
>
<Heading textStyle="h1"> Past Projects</Heading>
<Table size="sm" my={10}>
<TableCaption>Mere mortals can't achieve what I have </TableCaption>
<Thead>
<Tr>
<Th>Client</Th>
<Th>Description</Th>
<Th isNumeric>Hours spent</Th>
</Tr>
</Thead>
<Tbody>
{projects.map((project) => (
<Tr key={project.id}>
<Td>{project.client}</Td>
<Td>{project.description}</Td>
<Td isNumeric>{project.duration}</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
);
};
What’s most important here is the data fetch logic:
const [projects, setProjects] = useState([]);
useEffect(() => {
const fetchData = async () =>
await fetch("/api/projects")
.then((res) => res.json())
.then(({ data }) => setProjects(data));
fetchData();
}, []);
Note the URL called: /api/projects
.
Zero supports another form of data fetching that works great with SSR, but the example here shows a client-side data fetch.
Now to link to the Projects
page, we just need to edit the Button
on the homepage to link to this page.
// client/pages/Home/Home.tsx
// add as and href props to the button.
...
<Button
as="a"
href="/projects"
...
>
See past projects
</Button>
And now you should have this:
Query Parameters
The Node backend we’ve got now is truly basic. But zero supports a lot more e.g. we can handle query parameters sent from the frontend in the API function by retrieving that from req.body
// api/projects.ts
module.exports = function (req, res) {
const {id} = req.query.id
res.send({ data: PROJECTS });
};
// frontend call e.g. /api/projects?id=1
HTTP methods other than GET
It is worth mentioning that your exported API function will be called for other HTTP methods e.g. POST, PUT, PATCH, and DELETE methods. These have to be handled specifically. For example, req.body
will be populated with data sent via a POST request.
Global API endpoint configuration
As with the frontend implementation, no global configuration options are provided by default with Zero. A common use-case for this on the backend is centralising logic via middleware. This is a common express pattern.
The recommended way to do this is to move all middleware to a central directory or file e.g. the server
directory created earlier.
Here’s an example middleware:
// server/middleware.ts
const corsMiddleware = (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
next();
};
Note the call next(). Like express, this makes sure the next middleware in the chain is called.
The benefit of centralisation comes when you’ve got more than one middleware.
//server/middleware.ts
// middleware 2
const basicLoggerMiddleware = function(req, res, next) {
console.log("method", req.method);
next();
};
Now, instead of exporting each middleware singly, we centrally call each middleware, and then whatever handler is passed:
// server/middleware.ts
module.exports = (req, res, handler) => {
basicLoggerMiddleware(req, res, () => {
corsMiddleware(req, res, () => {
handler(req, res);
});
});
};
Then you can invoke the middleware in your handler e.g. api/projects.ts
:
const middleware = require("./server/middleware");
const handler = (req, res) => {
res.send({data: PROJECTS});
}
module.exports = (req, res) =>
middleware(req, res, handler);
Not the most elegant solution there is, I agree.
Conclusion
This is the basics of getting a full-stack app built with Zero. I strongly recommend checking out the official docs for cases I may not have mentioned in this article.
The premise of zero is particularly impressive, largely because of the varying file formats supported on the fronted and backend. React, Vue, Svelte, all the way to Python.
However, for it to be widely adopted especially for production cases there’s still work to be done.
Some quick downsides you may notice include:
- Slow compile time
- Poor error handling e.g. use a named export not default and the browser keeps loading forever.
- Poor handling of global defaults as mentioned in the article
- Poor OS support. The project hasn’t received any reasonable updates in months. Numerous issues unanswered too.
Regardless, I must say it’s a potentially great library with a clever take on ‘simple’ web development. Good thing is open-source, so good people like yourself and I can contribute to improve it. If we find the time.
Hope you had a nice read!