Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Different I/O for different methods #29

Closed
glitch452 opened this issue Jun 1, 2021 · 5 comments · Fixed by #32
Closed

Question: Different I/O for different methods #29

glitch452 opened this issue Jun 1, 2021 · 5 comments · Fixed by #32
Assignees
Labels
good first issue Good for newcomers question Further information is requested

Comments

@glitch452
Copy link

Hi,

I've started trying to use this module to manage my endpoints since I love the philosophy of how it's setup. I have a question about a use case.

For an endpoint with multiple methods (i.e. post and get) is it possible to have an input and output for post that is different than the input and output for get? Or, I guess a way to attach different endpoint definitions to the same route would also work?

@RobinTail RobinTail added the question Further information is requested label Jun 2, 2021
@RobinTail
Copy link
Owner

RobinTail commented Jun 2, 2021

Hello @glitch452 ,

Thank you for the question.

Essentially, an endpoint currently can hold the only input and output schema. And a routing path can be attached to the only endpoint.

I made Endpoint possible to handle different methods, but my idea here was to handle RESTful requests with similar parameters:

  • GET and DELETE: require an entity id to obtain or remove it;
  • POST and PUT: require an entity properties to be create or update it.

The variable behavior of the handler depending on the actual Method can be achieved by using methodProviderMiddleware (in example).

However, the endpoint I/O schema is still the only one, therefore I can offer you two solutions for your objective.

  1. Use different paths and different endpoints for different I/O schemas.
export const routing: Routing =  {
  v1: {
    user: {
      get: yourEndpoint1, // method: GET, input schema with id, output schema with properties
      create: yourEndpoint2, // method: POST, input schema with properties, output schema with new id
    }
  }
};

So you will have the following independent paths like this:

  1. Use .or() next to z.object().

You can specify multiple I/O object schemas using .or() concatenation and then just check the presence of some uniq parameter using in keyword, so that the compiler understands which particular schema is used for a given condition.

Here is an example:

export const universalEndpoint = endpointsFactory
  .addMiddleware(methodProviderMiddleware)
  .build({
    methods: ['get', 'post'],
    input: z.object({ // for GET
      id: z.string()
    }).or(z.object({ // for POST
      name: z.string(),
      age: z.number(),
      address: z.string()
    })),
    output: z.object({ // for GET
      name: z.string(),
      age: z.number(),
      address: z.string()
    }).or(z.object({ // for POST
      id: z.number()
    })),
    handler: async ({input, options: {method}}) => {
      if (method === 'get' && 'id' in input) {
        // fetching the user from DB
        return {
          name: 'John Doe',
          age: 23,
          address: 'Baker Street 221B'
        };
      }
      if (method === 'post' && !('id' in input)) {
        // saving the user to db
        return {
          id: 101
        };
      }
      throw createHttpError(400, 'Arguments mismatch with the method used');
    }
  });

I realize that it's probably not the best solution for you. I'm going to think a little bit on how I could make it more elegant.
But I'd also like to keep the routing and endpoint setup simple.

I hope something of that could help you. And if you have any idea or suggestion, don't hesitate to make a PR.
Wish you the best with your project, @glitch452

@RobinTail RobinTail self-assigned this Jun 2, 2021
@RobinTail RobinTail added the good first issue Good for newcomers label Jun 2, 2021
@RobinTail RobinTail linked a pull request Jun 5, 2021 that will close this issue
@RobinTail
Copy link
Owner

@glitch452 I've prepared some improvements that I believe should resolve the issue.
I think I found a relatively elegant way to implement it — the additional way to specify the routing:

import {DependsOnMethod} from 'express-zod-api';

// the route /v1/user has two Endpoints 
// which handle a couple of methods each
const routing: Routing = {
  v1: {
    user: new DependsOnMethod({
      get: myEndpointForGetAndDelete,
      delete: myEndpointForGetAndDelete,
      post: myEndpointForPostAndPatch,
      patch: myEndpointForPostAndPatch,
    })
  }
};

If you have some comments or ideas about this solution, I welcome you to the PR #32

@glitch452
Copy link
Author

Hey, thanks so much for the reply! I'm just getting back to this now. I'll go over it and let you know :)

@glitch452
Copy link
Author

glitch452 commented Jun 6, 2021

I looked it over and, this seems like a great solution. If I understand correctly, it allows a complete endpoint definition for each method on a specific route. That would let someone have specific input and output schemas, which clearly solves this problem, but it should also allow specific middleware or even a specific result handler to be configured for each method, which is great, since that enables maximum flexibility while maintaining the simplicity of the endpoint definitions.

What I ended up doing, for now, was something similar to the suggestion 2. I set the input schema to be an empty object with passthrough enabled, then I used the method middleware to determine the method, then I parsed the input separately for each method. This is a little bit of a janky workaround, but it does get the job done! I haven't started working on the OpenApi docs yet, but being able to automatically generate documentation for the endpoints would be really nice, and it looks like this tool can do that too! I would assume that these workarounds would make the documents less helpful though, and defining specific inputs and outputs for each method would allow the docs to also show those details, which would be great!

Just to give you some more background info, here's the general way we have our endpoints setup.

  • For creating objects we use the POST method at the plural of the object type. i.e. POST: /v1/users
    This returns the object that was posted (plus any meta data such as the database id)
  • For querying for objects we use the GET method at the same route. i.e. GET: /v1/users?name=jane%20doe
    This returns a list of the objects.
    These two cases already create a need for the different IO schemas.
  • To PUT, PATCH, and DELETE items, we use the id of the item in the path. i.e. DELETE: /v1/users/1
    The DELETE method would use the 204 status code, which would have no body on the response, and the PUT and PATCH methods would return the full item with meta data.
  • There are some occasional special cases, for instance, we allow users to download some data in a csv or ical format, which just returns a string body and sets some headers on the response for the appropriate content type. Maybe I should log a separate issue for this, but the way I implemented that was to have an output schema of { body: string, headers: Record<string, string> } and then I have a specific response handler to return it properly. I'm not sure how the OpenAPI parser would handle that, but it would be nice to be able to either set the output schema to simply be a string and then have another method for setting the headers (possibly just a middleware that makes a function available to the endpoint to set them, or a more general way to pass meta data to the response handler) or to be able to specify a key on the output schema that is used to define the actual output schema when generating the documentation.

So, our routing looks a little weird, but it works! Here's how I set it up for one of our services:

const v1_router: Routing = {
  courses: {
    summaries: courses_summaries, // Special case for a specific return format
    query: course_query, // Special case with specific parameters required (returns 1 item)
    ':id': course,
  },
  schedules: {
    generate: schedules_generate, // Special case that receives post data and does some computation
    download: schedule_download_post, // Downloads (csv/ical) an anonymous schedule, which is assembled based on the post data
    '': schedules, // Has 'get', 'post' assigned to it
    ':id': {
      '': schedule, // Has 'get', 'patch', 'delete' assigned to it
      download: schedule_download_get, // Downloads (csv/ical) a specific schedule that is saved in the Db
    },
  },
};

Thanks again for getting back to me on this and for actually putting in the work on the PR. I really like how you've architected this library, and your solution does seem to be just as elegant. ... Ps. If I do have some other use cases that come up, and I think it's something I could tackle, I'd totally be willing to create a PR! This one seemed a little involved and I wasn't sure if you were willing to open the tool up to be more generic or if you preferred to keep it more specific.

@RobinTail
Copy link
Owner

the feature released in version 1.2.0

Repository owner locked and limited conversation to collaborators Jul 18, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
good first issue Good for newcomers question Further information is requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants