I recently did a quick take-home test for a potential job opportunity. It included building a GraphQL API with nodeJS. I am not an expert in node (mostly use Python at work) but I have used it for some REST APIs /w Express. This was my first time using Nexus GraphQL.
The feedback I got was strong skills in Nexus and postgres, but lacking in the abstraction and organisational side.
I am looking for advice about what could be improved on the abstraction/organisational side of things.
Project code is available at: https://github.com/Sajomancer/node-graphql
Here is the general structure:
π¦
ββ Dockerfile
ββ README.md
ββ backend
β ββ createServer.ts // dependency injection of services
β ββ graphql // GraphQL models with their queries/mutations
β β ββ Ingredient.ts
β β ββ Recipe.ts
β β ββ index.ts
β ββ index.ts
β ββ models // internal models
β β ββ Ingredient.ts
β β ββ Recipe.ts
β β ββ index.ts
β ββ schema.ts
β ββ services // Services for interfacing with DB
β ββ DatabaseService.ts
β ββ IngredientService.ts
β ββ RecipeService.ts
ββ data
β ββ ingredients.csv
β ββ init.sql
β ββ price_changes.json
ββ docker-compose.yml
ββ eslint.config.mjs
ββ generated
β ββ nexus-typegen.ts
β ββ schema.graphql
ββ jest.config.ts
ββ package-lock.json
ββ package.json
ββ tests
β ββ api.test.ts
β ββ ingredientService.test.ts
β ββ recipeService.test.ts
ββ tsconfig.json
backend/graphql/Ingredient.ts
import { objectType, extendType, nonNull, intArg } from "nexus";
export const Ingredient = objectType({
name: "Ingredient",
definition(t) {
t.int("id");
t.string("name");
t.string("supplier");
t.float("currentPrice"); // no aliases?
},
});
export const IngredientQuery = extendType({
type: "Query",
definition(t) {
t.list.field("ingredients", {
type: "Ingredient",
resolve: async (_parent, _args, ctx) => {
return ctx.ingredientService.getAllIngredients();
},
});
t.field("ingredient", {
type: "Ingredient",
args: { id: nonNull(intArg()) },
resolve: (_parent, args, ctx) =>
ctx.ingredientService.getIngredientById(args.id),
});
},
});
backend/graphql/Recipe.ts
import {
arg,
extendType,
inputObjectType,
intArg,
nonNull,
objectType,
} from "nexus";
import { Ingredient } from "./Ingredient";
export const Recipe = objectType({
name: "Recipe",
definition(t) {
t.int("id");
t.string("title");
t.list.field("ingredients", { type: Ingredient });
t.string("method");
t.float("totalCost");
},
});
export const CreateRecipeInput = inputObjectType({
name: "CreateRecipeInput",
definition(t) {
t.string("title");
t.string("method");
t.list.field("ingredientIds", { type: "Int" });
},
});
export const CreateRecipeResult = objectType({
name: "CreateRecipeResult",
definition(t) {
t.int("id");
t.string("title");
// could return ingredients/method as well but I don't think it's necessary
},
});
export const RecipeQuery = extendType({
type: "Query",
definition(t) {
t.list.field("recipes", {
type: "Recipe",
resolve: (_parent, _args, ctx) => ctx.recipeService.getAllRecipes(),
});
t.field("recipe", {
type: "Recipe",
args: { id: nonNull(intArg()) },
resolve: (_parent, args, ctx) => ctx.recipeService.getRecipeById(args.id),
});
},
});
export const RecipeMutation = extendType({
type: "Mutation",
definition(t) {
t.field("createRecipe", {
type: "CreateRecipeResult",
args: { data: nonNull(arg({ type: "CreateRecipeInput" })) },
resolve: (_parent, args, ctx) =>
ctx.recipeService.createRecipe(args.data),
});
},
});
backend/models/Ingredient.ts
export interface Ingredient {
id: number;
name: string;
supplier: string;
currentPrice: number;
}
backend/models/Recipe.ts
import { Ingredient } from "./Ingredient";
export interface Recipe {
id: number;
title: string;
method: string;
ingredients: Ingredient[];
totalCost: number;
}
export interface CreateRecipeInput {
title: string;
method: string;
ingredientIds: number[];
}
export interface CreateRecipeResult {
id: number;
title: string;
}
backend/createServer.ts
import { ApolloServer } from "apollo-server";
import { schema } from "./schema";
import { DatabaseService } from "./services/DatabaseService";
import { RecipeService } from "./services/RecipeService";
import { IngredientService } from "./services/IngredientService";
export function createServer(databaseUrl: string | undefined): {
server: ApolloServer;
dbService: DatabaseService;
} {
if (!databaseUrl) {
throw new Error("DATABASE_URL environment variable is mandatory.");
}
const dbService = new DatabaseService(databaseUrl);
const recipeService = new RecipeService(dbService);
const ingredientService = new IngredientService(dbService);
const server = new ApolloServer({
schema,
context: () => ({ recipeService, ingredientService }),
});
return { server, dbService };
}
backend/index.ts
import { createServer } from "./createServer";
const databaseUrl = process.env.DATABASE_URL;
const { server } = createServer(databaseUrl);
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`Server running at ${url}`);
});
backend/schema.ts
import { makeSchema } from "nexus";
import path from "path";
import * as types from "./graphql";
export const schema = makeSchema({
types: types,
outputs: {
schema: path.join(__dirname, "../generated/schema.graphql"),
typegen: path.join(__dirname, "../generated/nexus-typegen.ts"),
},
});