Hi 👋 This is an overengineered way to show off my past projects/info about me/experiment with new technology. It's a lightweight, mobile-friendly React app, powered by Next and hosted on Vercel. And it's all Typescript, because we like type safety. It's set up as a monorepo, using pnpm workspaces and Turbo as a command runner
turbo dev
starts the development server + db connection to the prod DB (be careful!)turbo build
runs a prod build without a db connection (for CI)turbo build:serve
runs a prod build + db connection to your local db + serves it all once built (for local testing)turbo build:analyze
builds shows bundle sizes for a prod build (for verification)turbo format
runs Prettier to format the filesturbo lint
runs ESLint to lint all TS(X) and JS(X) filesturbo lint:types
runs tsc to confirm no type errors on the same filesturbo codegen
generates new GraphQL APIs from Github/Contentful + operation file types from the queries/mutationsturbo db -- db:migrate
uses Sequelize to run migrations, and you can list the status of migrations withturbo db -- db:migrate:status
. Undo withturbo db -- db:migrate:undo
turbo db -- migration:generate --name <name>
uses Sequelize to generate a new migration file ready to be populatedturbo webhook -- create <name>
will create a webhook subscription for the given API - for local dev and requiresdev
to be running alreadyturbo webhook -- list <name>
will list that API's webhook subscriptions - for local devturbo webhook -- delete <name> <id>
will delete a webhook subscription for that API - for local devturbo release
bumps the site version, run via Github Actionturbo clean
cleans up any built files like Next caches + codegen'd filesturbo topo --graph=graph.html
generates a graph of the monorepo dependencies for visualization
You need Node 20+ and pnpm 8+ installed. Run pnpm install
to get started once you have those two installed. You also need neonctl
installed globally, with brew install neonctl
. Most later commands are run via turbo
.
Even though it's just me, I use feature branches that merge onto main:
-
Run
git checkout -b feature-name
to make a branch, then commit to it and push to origin. -
I create a PR and make sure there's a label + an issue the PR "fixes" or "closes".
-
It'll automatically kick off Github Actions for quality, safety, and linting/formatting using CodeQL from Github + my own actions for Autochecks.
-
Check out the Vercel deploy preview to verify it looks good. Once that's good and checks pass, merge and delete the branch and it'll automatically create a new release + deployment for it! 🎉
Other folks: please follow the Contribution Guidelines.
Pretty standard Next app here. /public
contains static files, /src
contains all app code. Global types are in /src/types
for things like fetch
/etc. /src/hooks
contain app-wide hooks. Components, pages are self explanatory. /src/pages/api
contains API routes for Next. All API code outside the API routes themselves are in /src/api
. More below.
-
Next is the framework that wraps React. It adds great lazy loading/speed/build time static generation/global CDN/etc to make the site fast + easy to build by default. Notably, there's a "client" + a "server", and client requests to
/api/X
hit the server viapages/api/X.tsx
and it makes requests directly from the host, enabling use of DB/etc. -
Vercel hosts + builds the site. Every commit to
main
triggers a new deploy & publish on Vercel 🎉! There's a bunch of env variables matching.env
but with real data, that are used throughout the system. -
Cloudflare manages DNS/security. Cloudflare's MX records redirect email to Gmail.
-
Contentful handles all the content, minus a few things that come from Github itself. Using their GraphQL endpoint, I fetch data all across the site + create components around it. New content triggers a new build via a webhook, so it's always up to date.
-
useSWR is how I keep data all up to date. When Contentful hasn't published something new and you're still on the site, it'll fetch latest data for you. Super cool tool, and it does fancy things with caching too so there's no extra network requests + the UI is always updated. I wrote a strongly typed wrapper around it for endpoints so there's clear things you can fetch from server & there's only one dynamic Next API route needed. Fun!
-
Mapbox GL shows the map on the homepage and is loaded client side only because of speed/browser APIs it uses. It's huge file wise, but also lazy loaded.
-
MUI System provides the styling system for layouts/usages of
sx
on props, running onemotion
under the hood. -
GraphQL Codegen makes all the
*.generated.ts
files. It reads Github + Contentful's API schema + creates types out of them automatically. I run it on command when I write new queries/etc to get their types. -
Neon powers a distributed DB. This DB is used to persist auth tokens for Spotify/Strava beyond the lifetime of a deploy + refresh the token as needed.
-
Sequelize is used to run migrations on the DB + interact with it. It's a lightweight ORM that makes sure my DB tables have the right shapes and fields.
- All endpoints are strongly typed + synced between Next client + server with
/src/api/endpoints.ts
. No endpoint should ever be used directly from client, but the types in this file can be used! - The strong typing allows
useData
with anEndpointKey
to be how all components/hooks use data viauseSWR
. - It also allows
getStaticProps
and the like to call the fetchers directly to get fallback data for a page viafetchFallback
. - There's only one API route ever - it's a dynamic route that takes zero params other than the route name. It can be multiple levels deep like
/api/content/thing
and it'll be parsed correctly. Also strongly typed, and calls the fetchers directly. /src/api/server
has all server-only code, including API clients + fetchers themselves. DO NOT import from a client unless you want to leak secrets.
Because Spotify + Strava use Oauth and I use their APIs to pull stats/etc, I needed a lightweight DB to store auth tokens. I use Neon + Sequelize for this.
There's only two tables, one for the tokens and one for the Strava activities, and they're used from the server only.
- Token: I grab the latest token, see if it's expired, and if so, fetch new data. That's done via Spotify/Strava's APIs + the saved refresh token. Once I persist the new data, I can then call the APIs with the auth tokens. Nice defaults built in so anything missing gives back the right info as possible.
- StravaActivity: I create a row when there's a webhook event with a new activity, and I fetch the whole corresponding activity from Strava's API. If there are data updates, for now I just re-fetch the activity and update the row with new JSON data. I keep track of last update time, so multiple updates in the same time window don't hammer Strava's servers.
To create and run a migration:
- Run
turbo db -- migration:generate --name <name>
to create a new migration file - Fill it in with the appropriate
up
anddown
code for what you're doing - Create a new branch on Neon's UI to test with
- Connect to that branch with
turbo connect -- <branch>
and then start a new server withturbo dev
- In a new terminal tab, run
turbo db -- db:migrate
to run migrations onto that branch. - If all looks good, you can deploy request from Neon, review, merge, and delete the branch.
- Migrations can be undone with
turbo db -- db:migrate:undo
DO NOT make direct API calls to Strava if you can avoid it. They have a very restrictive API limit. Instead, there are webhooks subscriptions set up to persist new activity data to the db. I then use that data via normal API fetchers, but it never hits their servers outside webhooks.
More annoyingly, each app from Strava only has one possible subscription that it can use. Instead of trying to switch the config every time I want to test locally, there's just two different Strava apps I've created, each used for a different setup. The one connected to my personal Strava account is a test app. The one connected to the +prod account is for the prod app. There's an /webhooks
API route that handles all the logic when called from a webhook subscription.
Both use the same DB under the hood, but they use different auth tokens, refresh tokens, and callback URLs. An env variable, process.env.STRAVA_TOKEN_NAME
, is used to switch between them. Note that if you're testing webhook events locally, you'll want to create another branch in the Neon DB probably so you don't clobber the DB with simultaneous updates from the local webhook + the live webhook! Or briefly disconnect the prod webhook, then reconnect when done local testing.
Testing locally requires running Cloudflare's Tunnel service. Via it, https://dev.dylangattey.com/api/webhooks points to your local (running) Next app if you run turbo dev
. Here's how it works:
- Visit the tunnel's config page and use the "Install and run a connector" section to start a persistent service. Change it here if
api/webhooks
ever moves. - That will enable a persistent service on your device that will kep the tunnel open.
- To debug prod, you can temporarily change the
STRAVA
env variables to the prod values (except the callback url!) and log into Strava and change the accepted domain to thedev
subdomain. Then, delete the current subscription, recreate, and try editing. Visit theapi/webhooks
to restart the oauth flow on dev.
Strava is the only thing that supports webhooks right now!
- To create a subscription, first run
turbo dev
starts elsewhere. Then runturbo webhook -- create strava
to make a new subscription. This fails if one already exists. For local subscription testing - you want to make sure you delete the subscription after you're done testing so Strava doesn't keep pinging an endpoint that's not currently live. - To list existing subscriptions, run
turbo webhook -- list strava
to get the ids - To delete a subscription, run
turbo webhook -- delete strava <id>
with an id from the list script - To test actual event handling, just add a
console.log
inpages/api/webhooks
. To easily test, change the name of a Strava activity to trigger an event. Details about the events at https://developers.strava.com/docs/webhooks/. - If you need to make changes to the prod webhook subscription instead of the local one, change the env variables in
.env.development.local
forSTRAVA_CLIENT_ID
,STRAVA_CLIENT_SECRET
,STRAVA_TOKEN_NAME
, andSTRAVA_VERIFY_TOKEN
to match the values on Vercel. Restart everything, and you'll be running against the prod webhook setup. These subscriptions are only ever able to be changed locally with this script, or manually with a curl, to prevent tampering.
Standard semver versioning is done via semantic-release
and Conventional Commits for the commit messages. Typically I bump the major when there's a major rewrite, and that's it.
- Major: bumped if "!" appears after the subject of the commit message
- Minor: bumped if "feat:" appears in the message
- Patch: bumped by default in all other cases ("chore:"/"fix:"/etc)
Test a dry run with GITHUB_TOKEN=* pnpm turbo release -- --dry-run --branches={branch here}
after filling in the token.