- Environmental Variables: you'll need correct variables defined in
.envfile. - Redis: install Redis and have it running on its default port.
$ npm install
$ npm start# watch and run the main server (app.ts)
$ npm run dev
# watch and run the worker that processes background jobs (worker.ts)
$ npm run worker
# run prettier to reformat code base
$ npm run format
# run lint checks (fix warnings and errors that appear please)
$ npm run lint
# run the full test suite
$ npm testtest: contains test utilities (not actual test suites).src: contains the source code written in Typescript.-
config: contains configuration variables of the app (use this instead of process.env[...]). -
constants: contains shared constants. -
generated: contains generated files (do not edit them manually). -
middleware: contains express middleware for route requests. -
models: contains models that are part of application business logic that interact with some type of data within application.Each model is a single source of truth for data fetching, business logic, security for that particular type of data.
For example, we have an Application Model and a method for fetching a single application by id. Within this application, we currently only ever want the application to be viewed by the applicant of this application, or the recruiter whom this application was sent to:
class Application extends BaseModel<ApplicationProps> { static async getById({ user }: AppContext, id: Types.ObjectId | string) { const application = await MongooseApplication.findById(id); if (!application) return null; if (user.role === Role.APPLICANT) checkCorrectUser(user, application.applicantId); else if (user.role === Role.RECRUITER) await checkRecruiterHasAccessToApplication(user, application); else throw new ForbiddenError("You don't have permission to access this application"); return new Application(application); } }
By convention, we will pass
AppContextas the first parameter that contains Request Context such as the currentUserfor methods that need to handle permissions.Let's say our business requirement changes, and we want to allow Admin users to be able to view any Application. Also, we're introducing Redis as a caching layer for data fetching. Then all we have to do is update the model as follows:
class Application extends BaseModel<ApplicationProps> { static async getById({ user }: AppContext, id: Types.ObjectId | string) { let application: ApplicantProps; application = await Redis.get(...something); if (!application) { application = await MongooseApplication.findById(id); await Redis.set(...cacheTheFoundApplicationInRedis); }; if (!application) return null; if (user.role === Role.APPLICANT) checkCorrectUser(user, application.applicantId); else if (user.role === Role.RECRUITER) await checkRecruiterHasAccessToApplication(user, application); else if (user.role !== Role.ADMIN) throw new ForbiddenError("You don't have permission to access this application"); return new Application(application); } }
Because this static
getByIdmethod is a single source of truth for fetching a single application, we don't need to update it anywhere else. If we didn't do this and simply didawait MongooseApplication.findById(id);everywhere we needed to fetch an application, we would have to track down all these use cases, and add the redis logic, and update the permission logic for each of the use cases. -
public: contains public assets. -
queues: contains applications queues and workers for background jobs. We use Bull for this: https://docs.bullmq.io/. Jobs can be viewed on bull (dash)board: http://localhost:3999/admin with credentials from set with env variablesADMIN_USERNAMEandADMIN_SECRET. -
routes: contains express routes. Most requests should not go in here because we use GraphQL. -
schema: contains all GraphQL related files - types, resolvers, etc. We use GraphQL Nexus to construct the schema with Typescripts' type safety. See Nexus docs here: https://nexusjs.org/. -
services: contains services that are also part of business logic that interact with third-party service within application.For example,
payment-servicecontains payment related business logic that uses Stripe API. -
templates: contains HTML templates as strings in ts files. -
types: contains shared TypeScript type definitions. -
utils: contains application utilities. -
views: contains EJS template files: https://ejs.co/.
-
Some of the tools you could use to test GraphQL API manually: GraphQL Playground, Postman, Insomnia, etc.
To test queries/mutations/subscriptions that need an authenticated user, you will need to retrieve an ID Token through:
GET http://localhost:3999/dev/token?email=${USER_EMAIL}
Using retrieved token, set Authorization header to Bearer {idToken}.
Note: these ID Tokens expire after an hour, so re-retrieve them once they have expired.