npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

goose-express

v0.0.14

Published

Configurable express layer for your mongoose models

Readme

Goose Express

I've extracted this module from a project I'm working on as I believe it has re-use value. If there are features you would like to see implemented please let me know via github, as I'm keen to extend it's functionality. Similarly if there are things that do not work as expected please raise these as issues.

Stop re-writing the same express endpoints over and over again

This library is an express suitable abstraction on top of the popular MongoDB ODM, mongooose. It uses the models you've already written to expose an HTTP endpoint that follows best practices for the HTTP verbs GET, PUT, PATCH, DELETE, and POST.

Get Started

There's no sense in re-hashing all of the excellent options and controls the mongoose library offers. For detail about defining schema and models visit their documentation directly. Our example centers around a contrived user record with a few fields:

  1. Create your mongoose schema:
import { Schema } from "mongoose";

const UserSchema = new Schema({
  name: { type: String, required: true },
  dob: { type: Date, required: true },
  isActive: Boolean,
});
  1. Create your mongoose model:
import { model } from "mongoose";

const UserModel = model("users", UserSchema);
  1. Create a GooseExpress instance
import GooseExpress from "goose-express";

const options = {};
const userController = new GooseExpress(UserModel, options);
  1. Add the Goose Router to your app
import express, { NextFunction, Request, Response } from "express";
import bodyParser from "body-parser";

const app = express();
app.use(bodyParser.json()); // <--- You'll need to parse JSON payloads

app.use("/users", userController.createRouter());

That's it - your app will now include the following endpoints for interacting with the User model:

| Method | Use | | ------ | -------------------------------- | | GET | Query users in Mongo | | POST | Create a new user in Mongo | | PUT | Create or Update a user in Mongo | | DELETE | Delete a user from Mongo | | PATCH | Update a user in Mongo |

Nested Documents

Embedded documents (see https://mongoosejs.com/docs/subdocs.html) are child schemas attached to the root document. The Goose Express library allows easy interaction with these via a consistent API. Take, for example, the following extension to our user schema which appends an array of phone numbers for the user:

const PhoneNumberSchema = new Schema({
  isPrimary: { type: Boolean, required: true }
  number: { type: String, required: true }
})

const UserSchema = new Schema({
  name: { type: String, required: true },
  dob: { type: Date, required: true },
  isActive: Boolean,
  phoneNumbers: [PhoneNumberSchema]
});

This configuration tells mongoose to nest subdocuments for each phone number under the 'phoneNumbers' key in the user. Goose Express translates this interface into RESTful calls, allowing you to GET, POST, PUT, PATCH, and DELETE phone number records without lifting a finger. See the following sample requests for more detail:

# Get all phone number
# /users is the base path
# {userID} is the mongo ObjectID for the user
# phoneNumbers specifies the key within the model to query
curl -X GET /users/{userID}/phoneNumbers

# Add a phone number to the user
curl -X POST /users/{userID}/phoneNumbers -d '{"isPrimary": true, "number": "07232864332"}' -H "content-type: application/json"

# Update an existing phone number
# {phoneNumberID} is the ID of the phone number to update
# Note that the URL targets a key within the particular phone number for this patch request
curl -X PATCH /users/{userID}/phoneNumbers/{phoneNumberID}/isPrimary -d '{ "update": false }' -H "content-type: application/json"

Path Behaviour

Goose Express has been designed to expose a predictable and easy-to-use interface. At the core of this is the use of path parameters to represent sections of the document. Each of the exposed endpoints honors this concept, allowing you to quickly design queries to perform CRUD operations on sections of each document.

More specifically, path segments that are Mongo ObjectIDs are assumed to reference a nested sub-document within an array, while other segments are assumed to refer to keys on the currently specified document. For example:

# GET a key on a specific user (the final parameter is 'name' so the targetted attribute is the user document's 'name' field)
curl -X GET /users/000000000000000000000000/name

# PUT a phone number into a users phone numbers (the final parameter is an OID, so the targetted document is specified by that OID in the phone numbers array)
curl -X PUT /users/000000000000000000000000/phoneNumbers/000000000000000000000001

# Similarly you could DELETE or GET this phone number after it was created by referencing it by OID
curl -X GET /users/000000000000000000000000/phoneNumbers/000000000000000000000001

Validation

Goose Express simple exposes a RESTful interface onto mongoose models. As such, the API honors the validation configuration in those models. Validation errors are caught and passed to the next error handling middleware

Error Handling

When errors occur within Goose Express they are forwarded on to the next error handling middleware. To best handle these errors you should define a piece of express middleware that reads these errors and forwards them back to the client. This allows you to define how much or little of the error information you return to the caller. In general the err.message is for internal use (and contains useful debugging information), while err.detail is fit for most client's to consume.

app.use((err: any, req: Request, res: Response, next: NextFunction) => {
  console.error(err);
  return res.status(err.code).send({ error: err });
});

The Errors produced by this module have the following format:

interface Error {
  code: number;
  reason: string;
  detail: [
    {
      msg: string;
      param?: string;
      path?: string;
      location?: string;
    }
  ];
}

The following errors can be produced by the module:

| Error | Code | Reason | Usage | | --------------- | ---- | ------------ | ------------------------------------------------------- | | BadRequestError | 400 | Bad Request | An invalid request is supplied by the client | | NotFoundError | 404 | Not Found | The client has specified a resource that does not exist | | InternalError | 500 | Server Error | Something unexpected has happened |

API

Constructor Options

When intialising Goose Express you may supply the following options to adapt it's behaviour:

| Option | Type | Usage | | ------------ | ------- | -------------------------------------------------------------------------------------------------- | | waitForIndex | boolean | Specify to tell Goose Express to allow the indexes on a field to compile before receiving requests |

Route Options

Each of the supported HTTP verbs exposes a set available paths and query parameters.

GET

The path parameter in a GET request specifies the sub-section of the resource to retrieve. Omitting the path parameter (GET /) will query the collection, while it's inclusion will drill down into the document specified. As such, the first segment of the path parameter (when specified) must be the OID of a document in the collection. Take the following simple collection:

[
  {
    "_id": "5ec2d0193fd8c5d9b72948ce",
    "name": "Patricia",
    "isActive": true
  },
  {
    "_id": "5ec2d0193fd8c5d9b72948ce",
    "name": "Stephen",
    "isActive": false
  }
]
# Get all users
curl -X GET /users

# Get a user by ID
curl -X GET /users/5ec2d0193fd8c5d9b72948ce

# Get the isActive state of a known user
curl -X GET /users/5ec2d0193fd8c5d9b72948ce/isActive

Alternatively you can query the collection using the query query parameter as follows. The query parameter takes JSON and converts it into a MongoDB query which is used to select matching documents.

# Get users called Stephen
curl -X GET /users?query={"name":"Stephen"}

Goose express also supports projections on the collection level results via the fields query parameter. The fields parameter is a JSON array of field inclusions or exclusions, when fields prefixed with a - are removed from the output. N.B You cannot mix inclusions and exclusions in the same query.

# Get the name field for every user
curl -X GET /users?fields=["name"]

# Get every user but don't return the _id or name fields
curl -X GET /users?fields=["-_id, -name"]

Coming Soon

  • Projections on nested queries

POST

The POST method allows creation of new documents as well as addition of sub-documents into nested arrays. Specifically, when a path is not provided POST / the request body is treated as the new document to create. When the path specifies a key on the document which represents an array of sub-documents, the request body is treated as a new sub-document to push to that Array. N.B the post method does not first check if such a sub-document already exists before creation

Returning once again to our simple user collection:

# Create a new user (Response will be the new document)
curl -X POST /users -d '{"name": "Alex", "isActive": false, "phoneNumbers: []"}' -H "content-type: application/json"

# Create a new phone number for a given user (Response will be the updated document)
curl -X POST /users/{userID}/phoneNumbers -d '{"isPrimary": true, "number": "07225367328"}' -H "content-type: application/json"

PUT

The PUT method allows update of an existing document, creation of a document, and update of nested sub-documents. In contrast to POST requests, a PUT request must specify the location to insert the information provided. In our case this means specifying a Mongo ID. The upshot of this is that PUT requests are Idempotent, meaning that if you issue the same PUT command multiple times the result will be the same.

[
  {
    "_id": "000000000000000000000001",
    "name": "Patricia",
    "phoneNumbers": [{
      "_id": "000000000000000000000004"
      "isPrimary": true,
      "number": "08772366432"
    }],
    "isActive": true
  },
  {
    "_id": "000000000000000000000002",
    "name": "Stephen",
    "phoneNumbers": [],
    "isActive": false
  }
]

N.B the PUT verb will completely overwrite the specified record. If you only wish to update a specific section use PATCH

# Update an existing user (Response will be 201 Updated)
curl -X PUT /users/000000000000000000000001 -d '{"name": "Alex", "isActive": false, "phoneNumbers: []"}' -H "content-type: application/json"

# Create a new user (Upsert so response will be 200 Created)
curl -X PUT /users/000000000000000000000003 -d '{"name": "Jane", "isActive": false, "phoneNumbers: []"}' -H "content-type: application/json"

# Update a phone number for a given user (Response will be the updated document)
curl -X PUT /users/000000000000000000000001/phoneNumbers/{phoneNumberId} -d '{"isPrimary": true, "number": "07211392328"}' -H "content-type: application/json"

# Create a phone number at a specified ID for a given user (Response will be the updated document)
curl -X PUT /users/000000000000000000000001/phoneNumbers/000000000000000000000005 -d '{"isPrimary": false, "number": "07210865328"}' -H "content-type: application/json"

DELETE

The DELETE method can delete either a specified document, a specified sub-document from a nested schema, or a set of documents matching a Mongo query.

# Delete a phone number for a user by ID
curl -X DELETE /users/000000000000000000000001/phoneNumbers/000000000000000000000004

# Delete a user by ID
curl -X DELETE /users/000000000000000000000001

# Delete all users who have are not active
curl -X DELETE /users?query={"isActive": false}

PATCH

The PATCH method allows update of the full document, as well as targeted update to embedded fields. When targetting an individual field you must wrap your request in an 'update' to ensure it is valid JSON. When updating a specified document (path ends in an OID) you should specify the intended updates as the request body.

# Update a document
curl -X PATCH /users/000000000000000000000001 -d '{"isActive": true}' -H "content-type: application/json"

# This could also be achived by targetting the isActive attribute
curl -X PATCH /users/000000000000000000000001/isActive -d '{"update": true}' -H "content-type: application/json"

# Update a nested phoneNumber
curl -X PATCH /users/000000000000000000000001/phoneNumbers/000000000000000000000004 -d '{"isPrimary": true}' -H "content-type: application/json"

# Or by specifying the field
curl -X PATCH /users/000000000000000000000001/phoneNumbers/000000000000000000000004/isPrimary -d '{"update": true}' -H "content-type: application/json"

Custom Router Config

If you do not wish to expose the complete set of these routes, or need to implement different access control for GET vs POST, you can do so by using the individual router functions exposed on the GooseExpress object.

import express, { NextFunction, Request, Response } from "express";
import bodyParser from "body-parser";

const app = express();
app.use(bodyParser.json()); // <--- You'll need to parse JSON payloads

app.get("/users/*", userController.get()); // <--- The /* is required to ensure subpaths match

app.use(someAuthenticationMiddleware); // <---- Ensure that update requests are authentiated

app.post("/users/*", userController.post());
app.put("/users/*", userController.put());
app.patch("/users/*", userController.patch());
// app.delete("/users/*", userController.delete()); <---- Disable delete routes by ommiting this handler