A boilerplate for TypeScript + Node Express + Apollo GraphQL APIs.
src/errors/base.error.ts
and src/errors/error-handler/index.ts
.src/graphql/index.ts
and look for the formatError
of Apollo.src/middlewares/error.middleware.ts
.If the nature of your app isn't CRUDy (for a lack of a better word), you probably need domain models and a good amount of layering + mappers. Here's a good article on the topic. Otherwise, proceed.
SuperTokens
.supertokens-website
heavy lifts yourself, such as refreshing the session when it expires.# Install dependencies for the host
pnpm install
# Generate GraphQL Types
pnpm generate:gql-types
# Build the project for the first time or when you add dependencies
docker-compose build
# Start the application (or to restart after making changes to the source code)
docker-compose up
# Or run it in detached mode then listen only to this application's logs
docker-compose up -d
docker-compose logs -f api
Then refer to Database Migrations and Seeding to setup the database.
Note: You might be prompted to share your drive with Docker if you haven't done so previously. The drive letter you need to share in this case would be the drive letter of where this repository resides.
If docker compose have bootstrapped all services successfully, you should be able to access:
Login to the pgAdmin page using the credentials below:
Field | Value |
---|---|
dev@app.com | |
password | password |
If first time setting up:
Servers
then create a new server.General
tab, give this server connection a name.Connection
tab.Field | Value | Notes |
---|---|---|
Host name/address | db | Service name in docker-compose.yml file for our Database service is named db |
Username | postgres | Default username is postgres |
Password | password | As defined in the docker-compose.yml config |
http://localhost:8080/api/v1/maintenance/health-check
Note: If you prefer a different port, container name, or anything docker environment related. Just modify the docker-compose.yml
file and adjust to your preferred setup.
Name | Description |
---|---|
src/config/* | Any app level environment configs should go here. |
src/db/schema/index.ts | Zod Database schemas goes here. |
src/db/types.d.ts | Generated types by prisma-kysely . |
src/errors/<error-name> .error.ts |
Custom Errors. |
src/generated/**/*.ts | Generated files. |
src/graphql/scalars/<scalar-name> .scalar.ts |
Custom Scalars and their resolvers. |
src/graphql/enums/index.ts | GraphQL Enum resolvers and internal values. |
src/graphql/index.ts | Apollo GraphQL setup. |
src/graphql/schema.ts | GraphQL Schema/Resolver builder script. |
src/graphql/init-loaders.ts | Collect all dataloaders here. |
src/graphql/pubsub.ts | Initialize pubsub engine here. |
src/middlewares/<middleware-name> .middleware.ts |
Node Express Middleware files. |
src/modules/<module-name> /* |
Your feature modules. |
src/modules/_/* | Reserved module. Contains root schema for graphql to work along with some sample resolvers. |
src/redis/index.ts | Default Redis client is initialized here along with some helpers. |
src/shared/ | Anything that's shared (generic) throughout the app should be placed here. |
src/utils/<utility-name> .util.ts |
Utility files. |
src/app.ts | Main application file. |
.dockerignore | Folder and files ignored by Docker. |
.eslintignore | Folder and files ignored by ESLint. |
.eslintrc.js | Linter rules are defined here. |
.gitignore | Folder and files that should be ignored by git. |
.huskyrc | Husky config. Git hooks made easy. |
.lintstagedrc | Lint-Staged config. Run commands against staged git files. |
.prettierrc | Prettier config. An opinionated code formatter. |
codegen.yml | GraphQL Code Generator (file watcher) config file. |
docker-compose.yml | Docker compose config file. |
Dockerfile | Production Docker config file. |
Dockerfile.dev | Development Docker config file used by docker-compose . |
gulpfile.ts | Gulp task runner config file. |
tsconfig.json | Contains typescript config for this project. |
Note: This project structure makes use of barrel files, those index.ts you see on most of the folders. Make sure not to forget to export your newly created files to their respective barrel files (index.ts) if applicable!
# Make sure to set this to "production" in production environments
NODE_ENV=
# If you want to change the app port. Defaults to 8080.
PORT=
# DB Connection URLs
POSTGRES_CONNECTION_URL=
REDIS_CONNECTION_URL=
# SuperTokens
SUPERTOKENS_CONNECTION_URL=
SUPERTOKENS_API_KEY=
SUPERTOKENS_APP_NAME=
SUPERTOKENS_API_DOMAIN=
SUPERTOKENS_WEBSITE_DOMAIN=
See files inside src/config/*
that uses process.env
. Those are the environment variables that you can configure.
If your feature requires modifications to the database, update the Prisma Schema accordingly then run pnpm exec prisma migrate dev
.
See docs if you're unfamiliar with this command.
Since we have prisma-kysely
configured, this should also generate the corresponding typescript types for your database models and should be usable with Kysely right away for your database queries.
If you want to seed data, update the seed file at prisma/seed.ts
accordingly.
Based on your database model changes, make sure to reflect your schema changes as well in the src/db/schema/index.ts
file.
src/modules/<module_name>
directory.src/modules/<module_name>/use-cases/<use_case_name>.use-case.ts
Important:
.graphql
) under a graphql folder of your module: src/modules/<module_name>/graphql/
.
graphql-tools/load-files
to locate your type definitions.factories
under src/modules/<module_name>/
.<entity-name>.factory.ts
Create the main factory function inside the file:
createGQL<graphql-object-type-name>
Example:
function createGQLUser(user: Selectable<User>): GQL_User {
// ...
}
resolvers
under src/modules/<module_name>/graphql/
.index.ts
file under the folder.
src/graphql/schema.ts
uses the index
barrels (TS files) to load the resolvers.query/mutation/subscription
resolver file under the folder.user
for examples.Tip: Keep your resolvers thin by making the business logic layer (use cases) do the actual work and calling those in the resolvers.
If you have enums defined in your GraphQL Schema, most likely you have your own internal values which means you'll have to resolve these enums to match your internal values which, most of the time, are internal enums you use in the app. Simply define the enums under the src/graphql/enums
directory and make sure to export it on the index
barrel.
This is also true to scalars at src/graphql/scalars
.
routes
under src/modules/<module_name>/
.index.ts
file inside that folder.
app.ts
.handlers
under src/modules/<module_name>/routes/
.<graphql-object-type-name>.handler.ts
user
for examples.Migrations and Seeding is handled by Prisma. Refer to docs on how to work with this.
The environment variables (see src/config/environment.ts
file) are currently configured with consideration to a running Docker container so running the scripts below directly from the Host machine wouldn't work. As you can see from the file, the connection strings are pointing to addresses that based on the Docker containers name (defined in docker-compose.yml
) and the default Docker Network created from running docker-compose up
makes this work.
This means you should run the Prisma commands within the Docker container. You can do this in two simple ways, either use (1) the Docker extension from VS Code or (2) do it via CLI by:
First, identify the Container ID:
docker ps
Then run the following command:
docker exec -it <container_id> sh
Tip: You don't need to supply the entire Container ID, just the first few unique sequence. For example, your target Container ID is dc123f66554b
. You can just run docker-exec -it dc sh
and you'll be inside the container.
Once inside the container, you can run your prisma commands.
pnpm exec prisma migrate dev
This will run database migrations. If there are changes in the Prisma Schema before you run this command, it will create a migration file and you will be prompted for a name for the migration. Make sure to double check the output.
Important: If you're not using Docker to run this app, you need to configure the connection strings of the database servers (e.g. Redis, PostgreSQL) via the environment variables.
pnpm exec prisma db seed
Important: Make sure to write idempotent seed scripts!
Resetting the database also runs the seed automatically.
pnpm exec prisma migrate reset
This boilerplate makes use of SuperTokens to handle sessions (with cookies) because security is very hard. The folks from Supertokens know really well what they're doing and it'd be in your best interest to know how this topic should be handled properly. Here are some relevant resources:
That said, see SuperTokens docs for specifics.
When trying this boilerplate and heading to the playground right away to test some GraphQL queries, you'll probably get an Unauthenticated Error. Something like the one below for example when querying users
:
{
"errors": [
{
"message": "Unauthenticated. Please try logging in again.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["users"],
"extensions": {
"code": "UNAUTHENTICATED",
"data": {
"$errorId": "801aecca-b19f-410c-9a7e-e9724b6f0e9f"
}
}
}
],
"data": null
}
This is because you're trying to request a resource that requires authentication (and in turn, you'll need a session). See this issue for implementation details of this boilerplate. Basically, the idea is you need the session cookies to be attached on your requests and this can be done after logging in successfully.
You can hit the /api/v1/auth/login/superadmin
endpoint to login as superadmin and get a session cookie to make requests on the playground. Your access token might expire after some time so you'll need to refresh it. Under normal circumstances, the frontend SDK of Supertokens will handle this for you but since we're working purely on the backend side for this boilerplate, you'll have to clear the cookies manually and then hit the endpoint again.
If sessions with cookies is not an option for you, Supertokens also supports header-based sessions (basically token-based). See this link for specifics. In the scenario that you're not using any of the Frontend SDK of Supertokens, then refer here.
Note: If you're not using Docker to run this app or prefer using Supertokens' Managed Service, make sure to configure the environment variables. See src/config/supertokens.ts
.
Generally, use snake-case
.
In most cases, we include the file functionality
in its file name in the format:
<file-name>.<functionality>.<extension>
For example:
.use-case
.ts.enum
.ts.model
.ts.util
.tsTypeScript Interface
and Type
file names should match their definition name.
For example:
Interface/Type name | File name |
---|---|
IAccessTokenPayload |
IAccessTokenPayload .ts |
IContext |
IContext .ts |
UniqueID |
UniqueID .ts |
Maybe |
Maybe .ts |
Read more on the SuperTokens docs based on your infrastructure.
Make sure to configure the database setup.
There are a few ways to go about this and can vary based on your setup.
Simply build the image from your local machine (or let your CI tool do it) with the command below.
# The dot on the end is important.
docker build -f <docker_file> -t <image_name>[:<image_tag>] .
# Example:
docker build -f Dockerfile -t graphql-starter:latest .
Then push the image you just built from your local machine (or from your CI tool) to your docker image repository (e.g. Docker Hub, AWS ECR, GitHub Container Registry).
docker push <typically_a_url_to_your_docker_image_repository>
Let your host server pull this image and run it from there.
Note: Typically, the tag
for the image you built should match the url
to your docker image repository. Refer to your image repository provider for specific details.
Extra: If you want to test the image you just built with your local setup, then configure the docker-compose file such that it points to your image instead of building from a Dockerfile. For example:
From:
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
command: npm start
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
restart: always
To:
services:
api:
# Set it to the image you just built
image: graphql-starter:latest
restart: always
If you're not using Docker, you can still get your hands on the build files. Normally, you'd want this approach if you're deploying to a VPS and have to move the files via FTP manually to your server or you have a cloud provider that accepts node apps where you'll have to provide it your project files with a package.json
file in the project folder (typically zipped).
Build the project:
pnpm build:prod
This will create a build
folder in the project directory which you can deploy.
Note: You might need to manually install the dependencies yourself if you're using a VPS. Otherwise, your cloud provider's NodeJS container will typically just need a package.json
from the root folder and they'll do the installation on every deploy.
When resolver types are generated by GraphQL Code Generator, the type of the 1st parameter of a field resolver is the parent type by default. This is not always true because at runtime, what the parent resolver returns is the actual type/object that will arrive in the field resolver 1st (parent) parameter. In cases like this, you'd need to type assert the parent. See full-name.query.ts
.
pets
table and a pet has an ownerId
but in your GraphQL Schema, what you expose is owner
and not ownerId
. You won't have access to the ownerId
in your resolver because GraphQL Code Generator generates what you defined in the schema. You'll have then to type assert in your resolver the type you know you returned.If you want to rename a field in your GraphQL Schema:
*.graphql
).src/generated/graphql/index.ts
then apply the new name in the GraphQL schema file.
When trying to auto import a GraphQL resolver to its respective index barrel (index.ts
file), you might notice you're not getting code completion when typing the resolver name in the export default
object. This is normal because your IDE thinks you're typing the key/property name (remember key-value pair).
When trying to debug async functions in VSCode and the breakpoints on the inner lines won't hit, try adding trace: true
to launch.json
file.
Generated custom scalars by GraphQL Code Generator are given a type of any
. As with TypeScript, if you can help it, give it a type/interface then map it to GraphQL Code Generator. You can read more here. But basically:
codegen.yml
such that it points to its corresponding scalar file.
<Scalar_Name_in_GQL_Schema>: <path_to_custom_type>#<name_of_type>
If you're using this repo template with Docker Windows, you'll notice that when you save any file, the watchers aren't reacting. This has something to do with Linux not being able to detect file changes from Windows because the file system events are not being propagated down to Linux. You'll have to resort to polling mechanisms in that case for any tools that watches file changes. I recommend using WSL and developing inside WSL instead.
If you want to boost your knowledge about GraphQL, I highly recommend reading the e-book: Production Ready GraphQL by Marc-Andre Giroux.
If you have a use case for File Uploads, I recommend reading Apollo's blog post for options. There used to be a simple example in this boilerplate but has been removed. See Issue #42 and the associated pull request to see relevant code.
If something is unclear, confusing, or needs to be refactored, please let me know. Pull requests are always welcome but do consider the opinionated nature of this project. Please open an issue before submitting a pull request.
It might not look good to list it here but I think it's important for you to be aware of the issues currently being worked on (and hopefully get a fix from their respective authors/maintainers soon) as you'd still end up with these issues if you were to implement these yourself and use the same dependencies.
graphql-subscriptions
vs graphql-codegen
. More info.MIT License
Copyright (c) 2019-Present Cerino O. Ligutom III
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.