- Node
- GraphQL
- Apollo Server
- MongoDB
- Mongoose
- Jest
You need node
, npm
, and mongod
(see instructions) installed globally on your machine in order to run this project.
All other dependencies are installed at the project level:
npm i
Once you've installed all dependencies, type the following command to get started:
npm start
- An Apollo server will start listening to requests at
localhost:4001
and watch for file changes thanks tonodemon
. - A MongoDB instance will run at
mongodb://localhost:27017
thanks to the Mongo deamon.
You can start the service on its own with npm run start:service
or the database only at npm run start:db
.
Once the server is up and running, you will be able to explore the API at http://localhost:4001/
thanks to GraphQL Playground
Click on the Schema
and Docs
tabs on the right to find the API documentation.
Feel free to write queries and mutations in the space on the left, see the results returned by the API, and take advantage of the auto-complete feature 😃.
Keep in mind that GraphQL typically uses a single endpoint to handle all the requests.
This API is designed with its data at the center. For each data type, you will find the following structure in the source code:
dataType
├── dataType.controller.js
├── dataType.model.js
├── dataType.resolver.js
├── dataType.schema.js
The schema is a self-documenting script that visually represents the interface and data structure exposed for each data type. In each *.schema.js
file, you will find the following elements:
- A definition of the fields that constitute the data type.
- How this data type can be queried and what the query returns (under the
Query
type). - How this data type can be mutated and what the mutation returns (under the
Mutation
type).
The fact that GraphQL is a strongly typed query language provides the same advantage as other strongly typed languages when it comes to developing (and discovering) the API.
Additionally, the ability to carefully choose which database fields are exposed for each data type minimizes the need to sanitize the input.
The model is the representation of this data type from the perspective of the database. Mongoose allows us to go further into shaping our data and making sure that it stays tidy.
A couple of the features that help us handling data for this application are the following:
- The ability to provide default values to fields allows to fill the gap between the fields that are not exposed by the API and yet necessary for the application.
- The ability to impose tighter constraints on the data (for example, the length of a string) comes in very handy as well.
- Even better, Mongoose plays a great role in making sure that we can write integration tests reliably without having to mock database behaviors.
Resolvers are a central piece of GraphQL. In this application, the resolver map of each data type (*.resolver.js
) simply represents the connection between what we call the controller and the schema.
You will find the business logic that relates to a particular data type in the controller. Typically, this is where the CRUD operations happen, including interactions with MongoDB.
The signature of each function (aka resolver) looks like this example:
resolver(parent, arguments, context)
parent
comes in play if the client requests different types in a single query.arguments
is obviously the sanitized input that was provided by the client.context
is very useful for middleware but and to store the value of headers that came from the request.
The value returned by the function is essentially the object that forms the core of the response of the Apollo Server to the client.
Environment-specific variables can be found in the config
folder at the root of the project.
The logic that relates to the configuration of the API can be found in the various files at the root of the source folder. The following files are particularly noteworthy:
src
├── resolvers.js
├── schema.js
├── database.js
├── server.js
├── index.js
├── util.js
- The
resolvers
andschema
files combine together the schema and resolvers of all the data type. The schema file also contains types that are shared among the data types. - The
database
file contains functions to connect to and disconnect from the data store. It also contains a function to drop the database, which is particularly useful when testing or developing the API. - The
server
file contains the logic you need to get the Apollo server started. - The
index
file brings the server and the database together and start the whole API. - The
util
file contains miscellani functions that can be useful in different parts of the API.
In addition to the API configuration, this project also contains at its root several config files related to the tools that are used to develop it:
- NPM:
package.json
andpackage-lock.json
- Babel:
babel.config.js
- Jest:
jest.config.js
- Prettier:
prettier.config.js
- Git and GitHub:
.gitignore
,_config.yml
,.github
,LICENSE
,CNAME
npm test
Running the tests will instantiate a separate Apollo Server and a database for test purposes. In order to pass a test, the output of the query must match the snapshot that was previously saved. To update snapshots:
npm run test:update
Since MongoDB relies on unique identifiers to avoid collisions, tests that involve foreign keys would systematically fail (snapshots would never match). Therefore, the test database relies on traditional auto-incrementing identifiers instead. The production database still relies on unique identifiers.