Skip to content

Quick start

Build a small series catalog with a working GraphQL endpoint. By the end, you will be able to create, query, update, and delete a serie, then call the same catalog through an MCP tool.

Download the starter

Download the complete starter and run npm install in the extracted folder. Then configure MongoDB in step 2 and run npm start. The files below are the same files included in the download.

Before you beginYou will build
Node.js 22+, npm, and a transaction-capable MongoDB deploymentA GraphQL endpoint at http://localhost:4000/graphql
Docker for the local database recipe, or an existing replica setA series catalog with embedded seasons and a generated MCP read tool
From a type to an API
DefineGraphQLObjectTypeFields + metadata

Describe data, relationships and application behavior.

Registerconnect()Endpoint names

Choose the singular and plural names explicitly.

BuildcreateSchema()Executable GraphQL

Models, inputs and generated resolvers are ready for your server.

1. Create the project

Use a supported Node.js LTS release and npm. The library itself requires Node.js >=18.18.0.

sh
mkdir series-api
cd series-api
npm init -y
npm pkg set type=module
npm pkg set scripts.start="node server.js"
npm install @simtlix/simfinity-js graphql@^16.11.0 mongoose@^8.16.2 graphql-yoga@^5

GraphQL and Mongoose are peer dependencies. This guide uses Yoga as the HTTP server; Simfinity generates the schema supplied to it.

2. Start MongoDB

You need a MongoDB deployment that supports transactions. A standalone MongoDB server can accept connections and serve reads, but it cannot execute Simfinity's transactional mutations. Use a replica set or a sharded cluster, as described in the MongoDB transaction requirements.

For local development with Docker, start a single-node replica set:

sh
docker run --name simfinity-mongo -p 127.0.0.1:27017:27017 -d mongo:8 --replSet rs0 --bind_ip_all

After MongoDB has started, initialize the replica set once:

sh
docker exec simfinity-mongo mongosh --quiet --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'localhost:27017'}]})"

Check that the node is writable before starting the API. Election can take a few seconds; run this again until it prints true:

sh
docker exec simfinity-mongo mongosh --quiet --eval "db.hello().isWritablePrimary"

The application below defaults to this local deployment. To use an existing database, set MONGODB_URI to its connection string before starting the server.

Local development configuration

This Docker example binds MongoDB to your machine's loopback interface and does not configure database credentials. Configure authentication and network access for a deployed database. If port 27017 is already occupied, use an available host port and update the application's connection string.

3. Define the schema and server

Keep database initialization and schema registration in schema.js. Both the HTTP server and MCP entry point import this same file. The embedded Season type adds a nested array inside each serie document.

text
series-api/
  package.json
  schema.js   # database + types + generated schema
  server.js   # GraphQL HTTP endpoint
  mcp.js      # generated tool example
js
import { GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql';
import mongoose from 'mongoose';
import * as simfinity from '@simtlix/simfinity-js';

const SeasonType = new GraphQLObjectType({
  name: 'Season',
  fields: { number: { type: GraphQLInt } },
});

export const SerieType = new GraphQLObjectType({
  name: 'Serie',
  description: 'A television serie in the catalog.',
  fields: {
    id: { type: GraphQLID },
    name: {
      type: new GraphQLNonNull(GraphQLString),
      extensions: { validations: simfinity.validators.stringLength('Name', 2, 120) },
    },
    year: { type: GraphQLInt },
    category: { type: GraphQLString },
    seasons: {
      type: new GraphQLList(SeasonType),
      extensions: { relation: { embedded: true } },
    },
  },
});

const uri = process.env.MONGODB_URI
  || 'mongodb://127.0.0.1:27017/series?replicaSet=rs0&directConnection=true';
await mongoose.connect(uri);

simfinity.addNoEndpointType(SeasonType);
simfinity.connect(null, SerieType, 'serie', 'series');
export const schema = simfinity.createSchema();
await simfinity.getModel(SerieType).init();
js
import { createServer } from 'node:http';
import { createYoga } from 'graphql-yoga';
import * as simfinity from '@simtlix/simfinity-js';
import { schema } from './schema.js';

const yoga = createYoga({
  schema,
  plugins: [simfinity.plugins.envelopCountPlugin()],
});

const port = Number(process.env.PORT || 4000);
createServer(yoga).listen(port, '127.0.0.1', () => {
  console.log(`GraphQL ready at http://localhost:${port}/graphql`);
});

Start the application:

sh
npm start

Open http://localhost:4000/graphql. Yoga provides GraphiQL, an editor with schema discovery and autocomplete. The server setup follows Yoga's Node.js integration.

4. Create your first record

Paste this operation into GraphiQL and run it:

graphql
mutation CreateSerie {
  addserie(input: {
    name: "The Expanse"
    year: 2015
    category: "Science fiction"
    seasons: [{ number: 1 }, { number: 2 }]
  }) {
    id
    name
    year
    category
    seasons { number }
  }
}

Example response; MongoDB generates a different ID for your record:

json
{
  "data": {
    "addserie": {
      "id": "507f1f77bcf86cd799439011",
      "name": "The Expanse",
      "year": 2015,
      "category": "Science fiction",
      "seasons": [{ "number": 1 }, { "number": 2 }]
    }
  }
}

Keep the returned id for the update and delete examples.

Endpoint names are explicit

connect(null, SerieType, 'serie', 'series') generates addserie, updateserie, and deleteserie. The GraphQL operation name CreateSerie is your own label and can use any naming convention.

5. Query the catalog

graphql
query BrowseSeries {
  series(
    category: { operator: EQ, value: "Science fiction" }
    pagination: { page: 1, size: 10, count: true }
    sort: { terms: [{ field: "name", order: ASC }] }
  ) {
    id
    name
    year
  }
}

The list is returned directly under data.series. With the count plugin enabled, a nonzero total matching count is added as extensions.count. See pagination and counts for current count behavior.

6. Update and delete

Use the ID returned by CreateSerie as the $id variable. Run the update before the delete:

graphql
mutation UpdateSerie($id: ID!) {
  updateserie(input: { id: $id, category: "Sci-fi" }) {
    id
    name
    category
  }
}
graphql
mutation DeleteSerie($id: ID!) {
  deleteserie(id: $id) {
    id
    name
  }
}
json
{
  "id": "REPLACE_WITH_THE_CREATED_ID"
}

Update inputs include id and the fields you want to change. Deleting returns the deleted record, so you can request its fields in the response.

7. Call the same API through MCP

Create mcp.js alongside the other files, or use the file in the download:

js
import mongoose from 'mongoose';
import * as simfinity from '@simtlix/simfinity-js';
import { schema } from './schema.js';

try {
  const { tools, callTool } = simfinity.generateMCPTools(schema, {
    include: ['series'],
    limits: { maxPageSize: 100, defaultPagination: { page: 1, size: 10 } },
  });
  console.log('Generated tools:', tools.map(tool => tool.name));
  const result = await callTool('series', {
    name: { operator: 'LIKE', value: 'Expanse' },
    pagination: { page: 1, size: 10 },
  });
  console.log(JSON.stringify(result, null, 2));
} finally {
  await mongoose.disconnect();
}

Run node mcp.js after creating the record. If you ran the delete example, create it again to include it in the tool response. It prints the generated series tool name and its result, then closes its database connection. This calls the tool directly; follow MCP integration to attach a protocol transport or configure permissions.

Where to go next

Troubleshooting

SymptomWhat to check
Transaction numbers are only allowed on a replica set member or mongosInitialize a replica set and use its URI. A standalone server is insufficient for mutations.
Database connection failsCheck that MongoDB is running, the URI is correct, and the replica set has a writable primary.
Cannot query field "addSerie"Use the exact generated name: addserie.
Required input is missingSupply name when creating and id when updating. GraphiQL shows the generated input types.
A rejected value produces Unexpected error.Yoga masks Simfinity validation errors by default. See error handling to expose selected application errors.
EADDRINUSE on port 4000Change the port passed to server.listen() and open that port in your browser.

Open source. Released under the Apache 2.0 License.