340

We are developing server with REST API, which accepts and responses with JSON. The problem is, if you need to upload images from client to server.

Note: and also I am talking about a use-case where the entity (user) can have multiple files (carPhoto, licensePhoto) and also have other properties (name, email...), but when you create new user, you don't send these images, they are added after the registration process.


The solutions I am aware of, but each of them have some flaws

1. Use multipart/form-data instead of JSON

good : POST and PUT requests are as RESTful as possible, they can contain text inputs together with file.

cons : It is not JSON anymore, which is much easier to test, debug etc. compare to multipart/form-data

2. Allow to update separate files

POST request for creating new user does not allow to add images (which is ok in our use-case how I said at beginning), uploading pictures is done by PUT request as multipart/form-data to for example /users/4/carPhoto

good : Everything (except the file uploading itself) remains in JSON, it is easy to test and debug (you can log complete JSON requests without being afraid of their length)

cons : It is not intuitive, you cant POST or PUT all variables of entity at once and also this address /users/4/carPhoto can be considered more as a collection (standard use-case for REST API looks like this /users/4/shipments). Usually you cant (and dont want to) GET/PUT each variable of entity, for example users/4/name . You can get name with GET and change it with PUT at users/4. If there is something after the id, it is usually another collection, like users/4/reviews

3. Use Base64

Send it as JSON but encode files with Base64.

good : Same as first solution, it is as RESTful service as possible.

cons : Once again, testing and debugging is a lot worse (the body can have megabytes of data), there is increase in size and also in processing time in both - client and server


I would really like to use solution no. 2, but it has its cons... Anyone can give me a better insight of "what is best" solution?

My goal is to have RESTful services with as much standards included as possible, while I want to keep it as simple as possible.

Kirk
  • 4,957
  • 2
  • 32
  • 59
libik
  • 22,239
  • 9
  • 44
  • 87
  • 2
    You might also find this useful: http://stackoverflow.com/questions/4083702/posting-a-file-and-data-to-restful-webservice-as-json – Markon Oct 29 '15 at 10:13
  • 6
    I know this topic is old but we've faced this issue recently. The best approach that we've got is similar to yours number 2. We upload files straight to the API and then attach these files in the model. With this scenario you can create upload images before, after or at the same page as the form, doesn't really matter. Good discussion! – Tiago Matos Mar 15 '17 at 02:31
  • 2
    @TiagoMatos - yes, exactly, I described it in one answer which I recently accepted – libik Mar 15 '17 at 16:51
  • 1
    "also this address /users/4/carPhoto can be considered more as a collection" – no it doesn't look like a collection and would not necessarily be considered to be one. It's totally fine to have a relation to a resource that is not a collection but single resource. – Felix K. May 15 '18 at 14:06
  • For option 1 and 3 there is another important "con": If you upload more than one image in a form the complete data transfer can get very large and exceed file upload restrictions on your server. Best option is 2 – Janning Vygen Jan 18 '19 at 11:18
  • Does this answer your question? [How do I upload a file with metadata using a REST web service?](https://stackoverflow.com/questions/3938569/how-do-i-upload-a-file-with-metadata-using-a-rest-web-service) – Vega May 24 '22 at 18:14

4 Answers4

294

OP here (I am answering this question after two years, the post made by Daniel Cerecedo was not bad at a time, but the web services are developing very fast)

After three years of full-time software development (with focus also on software architecture, project management and microservice architecture) I definitely choose the second way (but with one general endpoint) as the best one.

If you have a special endpoint for images, it gives you much more power over handling those images.

We have the same REST API (Node.js) for both - mobile apps (iOS/android) and frontend (using React). This is 2017, therefore you don't want to store images locally, you want to upload them to some cloud storage (Google cloud, s3, cloudinary, ...), therefore you want some general handling over them.

Our typical flow is, that as soon as you select an image, it starts uploading on background (usually POST on /images endpoint), returning you the ID after uploading. This is really user-friendly, because user choose an image and then typically proceed with some other fields (i.e. address, name, ...), therefore when he hits "send" button, the image is usually already uploaded. He does not wait and watching the screen saying "uploading...".

The same goes for getting images. Especially thanks to mobile phones and limited mobile data, you don't want to send original images, you want to send resized images, so they do not take that much bandwidth (and to make your mobile apps faster, you often don't want to resize it at all, you want the image that fits perfectly into your view). For this reason, good apps are using something like cloudinary (or we do have our own image server for resizing).

Also, if the data are not private, then you send back to app/frontend just URL and it downloads it from cloud storage directly, which is huge saving of bandwidth and processing time for your server. In our bigger apps there are a lot of terabytes downloaded every month, you don't want to handle that directly on each of your REST API server, which is focused on CRUD operation. You want to handle that at one place (our Imageserver, which have caching etc.) or let cloud services handle all of it.

small 2023 update: If possible, but CDN in front of the pictures, it usually will save you a lot of money and make the pictures even more available (i.e. no issues when peaks happen).


Cons : The only "cons" which you should think of is "not assigned images". User select images and continue with filling other fields, but then he says "nah" and turn off the app or tab, but meanwhile you successfully uploaded the image. This means you have uploaded an image which is not assigned anywhere.

There are several ways of handling this. The most easiest one is "I don't care", which is a relevant one, if this is not happening very often or you even have desire to store every image user send you (for any reason) and you don't want any deletion.

Another one is easy too - you have CRON and i.e. every week and you delete all unassigned images older than one week.

libik
  • 22,239
  • 9
  • 44
  • 87
  • 2
    What will happen if [as soon as you select image, it starts uploading on background (usually POST on /images endpoint), returning you the ID after uploading] when the request failed due to internet connection? Will you prompt the user while they proceed with some other fields (i.e. address, name, ...)? I bet you will still wait till the user hits "send" button and retry your request make them wait while watching the screen saying "uploadiing...". – Adro Aug 18 '17 at 09:45
  • 11
    @AdromilBalais - RESTful API is stateless, therefore it does nothing (Server does not track the state of consumer). The consumer of service (i.e. web page or mobile device) is responsible for handling failed requests, therefore consumer must decide if it calls immediately same request after this one failed or what to do (i.e. show the "Image upload failed - want to try again") – libik Aug 18 '17 at 11:04
  • 5
    Very informative and enlightening answer. Thanks for answering. – Zuhayer Tahir Oct 09 '17 at 13:42
  • 2
    This does not really solve the initial problem. This just says "use a cloud service" – Martin Muzatko Jan 16 '18 at 13:33
  • 7
    @MartinMuzatko - it does, it chooses the second option and tells you how you should use it and why. If you mean "but this is not perfect option that allows you to send everything in one request and without implication" - yes, there is no such solution unfortunately. – libik Jan 16 '18 at 13:55
  • Do you first save the files to the local disk before, or just stream them directly to the s3 bucket? Is it possible/recommended to do image validation and then send the image to the s3 bucket without using the server's filesystem? – Frondor Jun 01 '18 at 23:27
  • @Frondor - No using of local disk, it can only cause the problems. Yes, its better to stream it directly to cloud provider, if you are using Node.js and Multer, just check the docs: https://github.com/expressjs/multer and search for `MemoryStorage` – libik Jun 02 '18 at 20:55
  • well to overcome the only con you can wait till user submits, then queue it, like the hot shots are doing right now :) – Ayyash Jan 28 '19 at 05:58
  • Hi @libik. I'm at the same crossroads and really like your approach. In my app, I want the user to be able to upload an image at creation. As per your approach, I would have to wait until the POST completes and I receive the ID in the 201 created at to be able to do a PUT with the image to a separate endpoint. What if I wanted to offload the image to a headless server and marry it up with the POST request once it completes (i.e. 1 call in parallel to 2 endpoints as opposed to 1 call - wait for completion - another call). My only issue is how to pair them up as I have no common identifier. – Help123 Jul 23 '19 at 21:01
  • @Help123 - if your image-service allows you to give image a name and later get image by that name, then you can just generate some random string (i.e. gguid) and at the same time start sending image with that name (which will take longer) and save that name to database. However it can have some issues such as user clicks "ok" button, you behave as he completed everything, so he i.e. kills the app/browser and the upload of image is not finished. If you "block" him with a spinner, then he will wait till its finished. But maybe that force-closing rarely happens. – libik Jul 24 '19 at 12:55
  • @libik Thanks! I think I actually got it figured out. My plan is to send the image off and let the server respond with a url and ID which I'll then patch once the post is complete. I'm using a SPA so don't want to rely on user sending a name + guid over :). I do have a spinner in place so this should work and if the user exits before its finished, I'll have to use some sort of service periodically like you suggested to delete orphaned files/images that have no relation. – Help123 Jul 24 '19 at 16:25
  • 2
    The open to all file upload route can be used to attack the server right? This would lead to more load on the server and undesired aws S3 costs right? – Aayush Taneja Apr 18 '20 at 09:06
  • Anyone have any example code in Flask for this? Gotta receive a bunch of images for computer vision processing. – John Curry Sep 21 '20 at 18:35
  • This answer is very helpful. You saved a lot of my time. I was trying to find the solution to what to do with the unassigned images. I learned a new word "CRON" – princebillyGK Apr 20 '21 at 18:48
  • I have similar issue but haven't tried cloud storage like S3. If the uploaded file is NOT accessible to everyone (or it is at least NOT public) , does that mean your upload server should return URL of the file on your application server instead of the URL on S3 ? because your application still needs to check access permission before (web or mobile) client can really download the file . – Ham Jun 06 '21 at 04:07
  • @Han - the easiest way is to have unpredictable id of picture. For example facebook - there is a lot of content that is private or visible only for very limited amount of people. But once you see a picture and copy paste the image link, anyone who has this link can access the picture. In this way you show the pictures/links to pictures only for authorized people, but the image itself is public. – libik Jun 18 '21 at 14:45
  • The issue with 2 is not just user navigating away, but the user uploading the photo and deciding to delete the photo before submitting. Not only does the user need to wait for the image to upload finish before being able to submit the form (as it is waiting for the image url), it also waste bandwidth if the user decides to delete the image.. not sure how to handle this. – CyberMew Jan 17 '22 at 08:15
  • @libik I am not sure if you have covered/answered this already, but what happens if the user selects an image and then later on before hitting the submit button, realize that he selected the wrong image and want to change it? will that action replace the current uploaded image with an update request? – Paul Feb 21 '23 at 12:06
  • 1
    @Paul - you can have "remove" button that will actively remove it from frontend AND your storage. Or if you dont want to spend developer time on that, you can just do it on frontend - as it can still happen that user will upload the picture and then just exit the page, this use-case is already happening anyway (that pictures gets uploaded and then no owner is assigned to them later) – libik Feb 22 '23 at 09:17
149

There are several decisions to make:

  1. The first about resource path:

    • Model the image as a resource on its own:

      • Nested in user (/user/:id/image): the relationship between the user and the image is made implicitly

      • In the root path (/image):

        • The client is held responsible for establishing the relationship between the image and the user, or;

        • If a security context is being provided with the POST request used to create an image, the server can implicitly establish a relationship between the authenticated user and the image.

    • Embed the image as part of the user

  2. The second decision is about how to represent the image resource:

    • As Base 64 encoded JSON payload
    • As a multipart payload

This would be my decision track:

  • I usually favor design over performance unless there is a strong case for it. It makes the system more maintainable and can be more easily understood by integrators.
  • So my first thought is to go for a Base64 representation of the image resource because it lets you keep everything JSON. If you chose this option you can model the resource path as you like.
    • If the relationship between user and image is 1 to 1 I'd favor to model the image as an attribute specially if both data sets are updated at the same time. In any other case you can freely choose to model the image either as an attribute, updating the it via PUT or PATCH, or as a separate resource.
  • If you choose multipart payload I'd feel compelled to model the image as a resource on is own, so that other resources, in our case, the user resource, is not impacted by the decision of using a binary representation for the image.

Then comes the question: Is there any performance impact about choosing base64 vs multipart?. We could think that exchanging data in multipart format should be more efficient. But this article shows how little do both representations differ in terms of size.

My choice Base64:

  • Consistent design decision
  • Negligible performance impact
  • As browsers understand data URIs (base64 encoded images), there is no need to transform these if the client is a browser
  • I won't cast a vote on whether to have it as an attribute or standalone resource, it depends on your problem domain (which I don't know) and your personal preference.
Daniel Cerecedo
  • 6,071
  • 4
  • 38
  • 51
  • 3
    Can we not encode the data using other serialization protocols like protobuf etc? Basically I am trying to understand if there are other simpler ways to address the size & processing time increase that comes with base64 encoding. – Andy Dufresne Nov 21 '16 at 08:11
  • 1
    Very engaging answer. thanks for the step by step approach. It made me understand your points a lot better. – Zuhayer Tahir Oct 09 '17 at 13:31
  • 1
    Base64 increases the size of the data transferred by 33% by converting on client and server for "convenience" of code but without delivering other benefits. I would consider this a **great loss of performance**. (Not considering the 33% increased bandwith for mobile users and the time it takes for conversion on slower phones.) I've implemented apps using this approach using in significant lag on mobile devices if 4 img's, 4mb each are converted to base64 at the same time. And by definition `multipart/form-data` is the standard way of transferring binary data in HTTP requests. – nonNumericalFloat Oct 17 '22 at 09:01
24

Your second solution is probably the most correct. You should use the HTTP spec and mimetypes the way they were intended and upload the file via multipart/form-data. As far as handling the relationships, I'd use this process (keeping in mind I know zero about your assumptions or system design):

  1. POST to /users to create the user entity.
  2. POST the image to /images, making sure to return a Location header to where the image can be retrieved per the HTTP spec.
  3. PATCH to /users/carPhoto and assign it the ID of the photo given in the Location header of step 2.
mmcclannahan
  • 1,608
  • 2
  • 15
  • 35
  • 2
    I do not have any direct control of "how client will use API"... The problem of this is that "dead" pictures which are not patched to some resources... – libik Oct 23 '15 at 11:02
  • 5
    Usually when you choice the second option, is preferred to upload first the media element and return media identifier to the client, then the client can send the entity data including the media identifier, these approach avoids broken entities o mismatch info. – Mr Rivero Oct 27 '15 at 12:29
9

There's no easy solution. Each way has their pros and cons . But the canonical way is using the first option: multipart/form-data. As W3 recommendation guide says

The content type "multipart/form-data" should be used for submitting forms that contain files, non-ASCII data, and binary data.

We aren't sending forms,really, but the implicit principle still applies. Using base64 as a binary representation, is incorrect because you're using the incorrect tool for accomplish your goal, in other hand, the second option forces your API clients to do more job in order to consume your API service. You should do the hard work in the server side in order to supply an easy-to-consume API. The first option is not easy to debug, but when you do it, it probably never changes.

Using multipart/form-data you're sticked with the REST/http philosophy. You can view an answer to similar question here.

Another option if mixing the alternatives, you can use multipart/form-data but instead of send every value separate, you can send a value named payload with the json payload inside it. (I tried this approach using ASP.NET WebAPI 2 and works fine).

Community
  • 1
  • 1
Mr Rivero
  • 1,248
  • 7
  • 17
  • 2
    That W3 recommendation guide is irrelevant here, since it's in context of the HTML 4 spec. – Johann Jul 31 '17 at 21:58
  • 1
    Very true.... "non ASCII-data" requires multipart? In the twenty-first century? In a UTF-8 world? Of course that is a ridiculous recommendation for today. I'm even surprised that existed in the HTML 4 days, but sometimes the Internet infrastructure world moves very slowly. – Ray Toal Mar 06 '18 at 18:09
  • @RayToal Could you please elaborate how is multipart/form-data related to the HTML versions. My understanding is HTML 5 is an extension to support multimedia among other things. Are there any new additions in HTML 5 spec to handle binary data ? – Talespin_Kit Oct 15 '20 at 07:28