Project boilerplate for a full-stack TypeScript monorepo.
-
End-to-end technology choices and configuration for a typical CRUD app, aimed at maximizing iteration speed and developer experience.
-
The repo implements a bare-bones note-taking app to give some examples for common patterns (UI state, DB interactions, API calls, etc).
-
Infrastructure-as-Code config for deployment of frontend, backend, and Postgres DB on Render (see
render.yaml
). Demo deployment with this configuration here: https://fullstack-ts-client.onrender.com (the free-tier server will be slow to respond on the initial request.) -
Basic user account functionality. Authentication via Google Sign In, authorization against the API via JSON web tokens. Note that while the included implementation of JWT authorization here may be a technically functional starting point, it has not been audited rigorously for security issues, and isn't intended for production use as-is. Use at your own risk.
-
Scripts for common dev workflows. (See the "Dev Scripts" section below.)
Click "Use This Template" near the top of this repo's Github page to create your own copy. Continue from there with the setup steps in Local Development below.
Selection rationale and documentation links for the key technologies are below. In general, the goal here is to optimize for development speed- with rock-solid stability or scalabilty being secondary (though not completely abandoned) as priorities.
-
React Router Client-side routing. Note that this is the only library in the project that intentionally uses an older version (v5, rather than the current v6). V6 brings many radical API changes, while ditching a number of useful features present in earlier versions- straightforward manipulation of browser history from outside of the React component tree being one example. The implementation of Sign In With Google here currently relies on that feature. Tanstack Router is a promising alternative for the future- at present, it's still in a beta version and documentation is very sparse.
-
pnpm Package management. Significantly faster drop-in replacement for npm, with better monorepo support. Recommend
alias pn="pnpm"
to avoid the typing speed tax. -
TailwindCSS Styling. Significantly improved dev. speed (in my opinion) and moderately improved performance over many other solutions, and has seen a lot of consolidation of usage in the React community.
-
headlessui.dev Headless/accessible React component library integrated with Tailwind.
-
Flowbite Collection of prestyled Tailwind components, useful for prototyping (essentially Bootstrap for Tailwind.)
-
Zustand Frontend state management. Much more lightweight and quick to develop with than Redux/Redux Toolkit, and has recently gained a lot more traction.
-
tRPC API layer functionality, shared request/response typing across client and server. Theoretically tRPC can enable faster development speed by streamlining a lot of the typical API boilerplate. In my experience so far, this is partially offset by a lack of clear documentation or examples for some use cases. Still, past the initial learning curve it's been a promising choice for rapid iteration/prototyping.
-
react-query Client-side API calls. Response caching and coordination of refetches, integration with loading/error UI states. Included as part of tRPC.
-
Express API server. Fastify would be the trendier (and more performant) modern choice, but maturity/documentation still felt lacking in some areas compared with Express.
-
Prisma DB schema definition, queries, migrations. Streamlined DX (particularly around schema updates/auto-generation of migrations); one of the better JS ORM options I've seen for quick prototyping. The performance at higher scale is an open question.
-
PostgreSQL Database. A classic.
-
react-hook-form Forms. Better performance, DX, and support than Formik. react-hook-form seems to be becoming the new standard React form lib. Still some gaps/wonky behavior around complex validation scenarios (but that seems to be the case with every form library.)
-
Zod API request/response validation, form validation. Good Typescript integration, integrates with tRPC, and has gained a lot of traction recently- appears to be becoming the new Yup/Joi.
-
Render Low-config, PAAS cloud hosting platform. Very straightforward / quick to get a multi-service deployment up and running, with zero infra or devops knowledge required. The included
render.yaml
is in their Infrastructure-As-Code format, and has most of the config you'd need to deploy these services. Docs/specification forrender.yaml
: https://render.com/docs/blueprint-spec
This template uses a monorepo structure intended to support sharing code and configuration across multiple apps/services.
The services
directory contains independently deployable units. Currently:
- a frontend client (
services/client
) - a backend API service (
services/server
)
The packages
directory is used for shared internal libraries/configuration. Currently:
- baseline for per-package eslint config (
packages/eslint-config-shared
) - baseline for per-package TypeScript config (
packages/tsconfig
) - UI component library (
packages/ui
) - shared utility functions and constants (
packages/utilities
)
Note that the tsconfig files include cross-package import path aliases, which are configured to work with VSCode auto-import.
Example import from within a file in services/client
:
import {Button} from "@ui/Button"
would be functionally equivalent to
import {Button} from "../../../packages/ui/src/Button"
.
-
install Nodejs / npm.
-
Install pnpm globally:
npm i -g pnpm@latest
-
Install Docker/docker-compose (used for the dev database). Mac installation: https://docs.docker.com/desktop/install/mac-install/
-
If you haven't already, follow the
Starting a project with this template
section above. Clone down your newly created repo. Depending on the desired license for your project, remove or replaceLICENSE.md
and all references in the project to the MIT License (package.json entries for"license": "MIT"
) as needed. -
As desired for your project, find and replace all strings/names in code that have the prefix
fullstack-ts
orfullstack_ts
(many of these are inrender.yaml
). -
(Optional): Install recommended VSCode extensions from
.vscode/extensions.json
. VSCode will usually prompt you to do this when first opening the project. -
At project root:
chmod u+x repo_scripts/setup_env.sh; repo_scripts/setup_env.sh
Executing this shell script will do the following:- Create two new
.env*
filesenv.server.local
andenv.shared.local
with all values specified in the respective*.example
env files (.env.server.example
and.env.shared.example
). The example values will work as-is with the initial template for most of the values, except for those noted by comments in the example files. - Generate a jwt_secret using
openssl rand -hex64
and assign it toJWT_TOKEN
inenv.server.local
- Create two new
-
At project root:
pnpm run start:dev
This will start the dev database and apply any pending migrations, followed by starting up the client and server locally. Open http://localhost:3000, and click "Login with Google". After logging in, try filling in and submitting the "New Note" form. If it works, you're all set! Remove anything you don't want and continue developing your app.
The included render.yaml
file contains much of the configuration needed to deploy a project from this template on Render. These are the remaining steps.
-
If not completed already, do Step #2 (renaming values for your project) from the Local Development Setup section above.
Note that the
name
field for services withtype: web
will determine the generatedrender.com
domain for the service (example:name: fullstack-ts-client
results infullstack-ts-client.onrender.com
).By default, this file will create services on the Free plan (suitable only for basic testing- the server sleeps when inactive, and the DB is wiped after 90 days). Modify the
plan: free
fields on services here as desired if you want to start with a different plan (this can also be upgraded later). -
Make any necessary environment variable changes in the envVarGroups section of
render.yaml
. Only non-sensitive environment variables should ever have values directly added torender.yaml
(among other reasons, because you'd check this into git.) For any environment variables marked withsync: false
here, you'll add the values later via the Render dashboard (Step 6). Note that the default env var group in this template will be accessible by both theclient
andserver
services, so you should only include values you want shared with both here.
This is also a good time to modify any other desired IAC config for your services. Documentation for all other fields available in render.yaml
is here: https://render.com/docs/blueprint-spec.
-
Push your project to Github or Gitlab.
-
Log into (or create) an account on Render at https://dashboard.render.com, and connect it in the account settings to the relevant Github/Gitlab account (if you didn't use that to create your Render account initially).
-
Go to the Blueprints section. Click "New Blueprint Instance". Select the Github/Gitlab repo for your project from the list.
-
Modify "Blueprint Name" and "Branch" as desired, and click "Apply" to create all services and
envVarGroups
specified in yourrender.yaml
. -
Go to the Env Groups section on Render. Select the one you just created. Add your production value for
GOOGLE_CLIENT_ID
here. -
Navigate to your
server
(or equivalent) service in the Render dashboard, and go to the Environment settings. Add your production value forJWT_SECRET
here.
Assuming the initial deploys for all services were successful, your deployment is complete! Visit the *.onrender.com
url for your client service to check it out. Future modifications to render.yaml
will be applied to services automatically on push (this can be configured).
- Get your computer's IP with
curl ifconfig.me
. - Add the IP as an allowed source in the Access Control section of the Render dashboard for your database (if not already present).
- Go to the DB's page in the Render dashboard. Use the connection info given there to connect from your local machine via your preferred method (e.g., a tool like PGAdmin).
Problem: the line import { PrismaClient } from "@prisma/client";
in queries.ts
is highlighted with an error in your editor. The error says the @prisma/client
module cannot be found.
Solution: Start/restart your local dev server. If that doesn't resolve it, restart the Typescript language server. To do this in VSCode: hit Cmd + Shift + P and run the command TypeScript: Restart TS server
.
These would be run from the project root (specified in the root package.json
.)
To run, all of these need to be prefixed by pnpm run
(e.g., you'd run start:dev
below as pnpm run start:dev
).
Spins up the development Postgres database via docker-compose and starts both the client (at http://localhost:3000) and server (at http://localhost:5000). If you get an error message about the Docker daemon not running, open Docker for Desktop and try again.
Prod build / startup scripts, referenced in render.yaml
. You'd only ever run these manually if testing the prod build locally.
- Note that start-server:prod also applies any pending migrations (by calling
prisma-migrate:prod
, see below).
During local dev, use this to generate a new migration in services/server/prisma/migrations
for any changes to prisma.schema that aren't currently reflected in the database. This will also immediately apply those changes to the db.
Open a psql
shell into the locally running dev database.
During local dev, use this to delete all data from the database and re-apply all migrations from scratch.
If you have an existing local dev DB for an application that wasn't previously using Prisma, this script will do the following:
- introspect your existing DB schema and update prisma.schema accordingly
- generate an 'init' migration file to represent the current state of the database, and mark it as already "applied"
Used to apply pending migrations to a production (or otherwise non-local development) database. This is run automatically as part of start-server:prod
.
Delete the local dev database Docker container and its associated volume / all data within it.
The next time pnpm run start:dev
(or just docker-compose up
) is run, it will be created as an empty db. pnpm run migrate:dev
could then be used to reapply all migrations.
Delete and reinstall all node_modules. Useful if debugging dependency issues.
Reinstall node_modules packages whenenever changes to pnpm-lock.yaml are detected in the latest merge.
Format and lint all staged files (Prettier, Eslint), as well as typechecking all TS files. Use git commit --no-verify
to skip this check if needed. In place of lint-staged
, this uses a faster custom script (see .husky/pre-commit
).
- finish configuring/documenting Playwright e2e tests
- admin interface/react-admin integration
- consider running
server
via Dockerfile (potentially in local dev as well) - scripts for DB shell access
- server/client logging configuration (Pico is one candidate)
- Hygen template for bulk customizing/renaming after clone (may need a new hygen.js function, like these examples: jondot/hygen#106)
- Hygen generators for routes, entities, etc. (possibly look to RedwoodJS generators as prior art)
- Storybook