Given I recently launched listifi, I thought it would be interesting to talk briefly about the listifi tech stack.
Tech stack #
I'd be remiss if I didn't use my app before discussing my list of stack choices. Here is my tech stack.
In general, when I build an app I tend to focus on gluing the pieces together myself. This is my personal preference, but I find that frameworks or libraries that try to do too much end up getting in my way very quickly.
Provide some helper utilities and then get out-of-my-way.
The overall folder/file structure follows the patterns I described in my previous blog article.
Front-end #
There are some common choice in my list, in particular: typescript and react. I also opted to use a component library chakra-ui primarily because it made it easy to make CSS changes by using react component props. It also wasn't a massive library that tried to solve every design or UX problem users would come across.
One potentially controversial choices was reaching for redux. It seems as of
late redux has fallen slightly out-of-favor and more people are migrating
towards react-specific libraries like recoil. I spent
some time researching recoil and ultimately decided it did not fit into my
personal design choices. In particular, I don't like how tightly coupled
recoil is to react. I find this tight coupling makes it an easier API to
work with, but will inevitably lead to issues if I wanted to build a
react-native mobile app or a CLI app. Since redux is framework agnostic,
battle-tested, and using libraries like
robodux I can avoid 90% of the
boilerplate.
robodux is great because it promotes the idea that redux is just a local
database. I can create database tables which translate to slices in the
redux world.
Building a redux slice that has: action types, action creators, and a reducer
with a common set of table operations like: set, add, patch, and remove
can be written in a single line of code:
1import { createTable } from `robodux`;
2
3interface List {
4 id: string;
5 name: string;
6 ownerId: string;
7}
8
9const slice = createTable<List>({ name: 'LISTS' });
10/*
11{
12 actions: {
13 add,
14 set,
15 remove,
16 patch
17 },
18 reducer,
19 getSelectors,
20}
21*/
redux-cofx is another library I wrote
that is a hybrid between redux-saga and redux-thunk. Instead of wiring
generator functions up to sagas, I simply create a function like thunks that
activate a generator function and still leverage the API of redux-saga. For
example, if I want to fetch some lists, I would write something like this:
1import { select, call, batch, createEffects } from 'redux-cofx';
2import { selectHasTokenExpired } from '@app/token';
3import { apiFetch } from '@app/fetch';
4
5// API is very similar to redux-saga
6export function* onFetchLists() {
7 const hasTokenExpired = yield select(selectHasTokenExpired);
8 if (hasTokenExpired) {
9 return;
10 }
11
12 const resp: ApiFetchResponse<ApiListsResponse> = yield call(
13 apiFetch,
14 '/lists',
15 );
16
17 if (!resp.ok) {
18 return;
19 }
20
21 const users = processUsers(resp.data.users);
22 const lists = processLists(resp.data.lists);
23 // this dispatches multiple actions at the same time without
24 // two copies of the state being generated
25 yield batch([
26 addLists(lists),
27 addUsers(users)
28 ]);
29}
30
31// This is a helper function that will
32// convert a map of action creator names to effects.
33// When we dispatch `fetchLists` it will
34// activate `onFetchLists`: e.g. store.dispatch(fetchLists());
35export { fetchLists } = createEffects({
36 fetchLists: onFetchLists
37});
It's a very useful little library that is a satisfying hybrid between
redux-saga and redux-thunk and I encourage anyone who feels like
redux-saga is too heavy for their uses to
give it a try.
Backend #
On the backend I decided to go with koa. I found the
minimalist approach of the library to be aesthetically pleasing and exactly what
I want from a web server. Koa doesn't even come bundled with a router, you have
to install one yourself, I love that! koa has a great middleware system,
adopted from express.
I originally went with prismajs but ultimately found
the library is too limited and restrictive. I would highly recommend people use
it if they are using graphql, but for a simple RESTful API, I found it
couldn't do even the simplest of SQL queries.
So, in the end, I switched to knexjs which, again, plays right into my preferences. It's a query builder. When I think about how to query my data, I really just want to write SQL.
As an aside, I really do not get the fascination with ORMs. SQL is already a DSL, why are we re-inventing the wheel and adding another layer of abstraction? SQL is amazing and more people should be comfortable writing in it.
I ended up writing my own server-side rendering implementation for react. All
in, with data loading and getting data loaded into redux I wrote about 300
lines of code. Once I landed on a working implementation, the rest was pretty
straight-forward. However, I get why people don't want to keep rebuilding SSR
over and over again and end up switching to something like
nextjs.
Deployment #
For deployment I tend to lean heavily on docker. I use docker-compose for development and docker-machine for deployment. Since this was a fresh project that I don't know how far I'm going to take it, I didn't want to create an automated build pipeline using CI. I'll briefly describe my deployment lifecycle:
- Develop using
docker-composelocally - Build features locally
- Use a production tuned
docker-composeyml file to build images locally - Push the images to Google’s Container Registry
- Then I run
eval $(docker-machine <name> env)to tunnel into my production VM’s docker - Then I run
docker-compose -f production.yml pull --ignore-pull-failuresto download the new images - Then I run
docker-compose -f production.yml up --no-deps -dto restart my containers with the new images
My VM was on Google's Cloud Compute and my domain was hosted on Cloudflare.
Conclusion #
This covers a high-level overview of my tech stack and something I will continue to reach for on new projects. It fits nicely into my development style and I understand how all the pieces work together because I wrote the glue myself.