Suggestions for a very simple API?

I would like to add a few very simple API endpoints to my server to let my users interact with their data from their own software. I’ve seen nimble:restivus and simple:rest - neither have been updated in a while, is that because they are perfect as-is? Do they still work with the latest versions of Meteor?

I don’t want to expose all of my subscriptions, collections or methods. Which of these packages is simplest to use so that I can give an authenticated user access to a limited number of get/put methods?

I’m familiar with using WebApp.connectHandlers.use to write my own webhooks for external services I use, so I was thinking of writing my own. But what’s the best way to authenticate users? Do I give them a dedicated API key via the website, or make their software have to log-in to the API using their email and password (each time, or giving them a token when logged in)?

Any suggestions greatly appreciated!

6 Likes

HI Chris

Not a simple API, but I’ve found this to be excellent to get started with getting an API (that is standards-based) actually written:

https://editor.swagger.io/

The best part about this is the code generation aspect - it actually generates all the boilerplate code you need to integrate into your app - you only need to update the actual functions to query and return data. See the menu bar at the top: File | Edit | Generate Server | Generate Client

Generate server code as nodejs-server and you’ll get a zip file of all the code you need.

You can also download and run the editor locally.
https://swagger.io/tools/swagger-editor/download/

Hope that helps!

3 Likes

I’m using built in webapp and dedicated keys. It works well.

1 Like

Yeah, I thought that could be the simplest solution, thanks @minhna.

Do you do any rate limiting? Do you use Random.hexString() to generate the keys?

I use Express.js within WebApp and authenticate using Passport JS. One endpoint /token provides token using Basic strategy, all other user endpoints then use the Bearer strategy to check for valid token by searching in the user’s services.resume.tokens for the hashed token.

What’s the benefit of swapping (I presume) a username:password for a token? Apart from preventing their software from having to store the plain-text password?

I was planning on giving the user a 32-character hex API key (while they are logged into the website), and then using that API key with the Basic strategy on every API request. Is that OK? This is what I do to log my server into various API’s for 3rd party services I use.

The user’s password will never be stored in whatever service they are developing, because they only need to copy/paste the API key, not their password. They can revoke the key at any time by logging into my website.

@globalise: Where do you see the benefit of using Express.js with passport.js, compared to the popular atmosphere packages? Is passport.js the library that is also used in the accounts-packages? I would think that the additional amount of authentication/authorization (plus a lot of test-code to keep it bullet proof for the future) would make it less attractive in comparison to a already meteor-ready atmosphere-package.

I am currently also looking for the easiest (development), but safe and most stable package to start with. In my short evaluation, I picked nimble:restivus as it got the most stars/downloads, has all features I could currently think of and last version published some months ago. So far, I just picked it to be discussed in our team-planning and not yet started with development. Let me know if you know more than me about it.

Just an update. Rather than using Random.hexString() I’ve settled on using a base64 string, as per this page:

const len = 32;
const key = require('crypto').randomBytes(Math.ceil((len * 3) / 4)).toString('base64').slice(0, len),

I enforce auth in the HTTPS header: authorization Basic userId:key so I can look up the key using userId which saves me having to put an extra index on the key field itself in mongo.

If anyone's interested, here's my full webapp handler code:
const version = 1;

const codes = {
	400: 'Bad request',
	401: 'Unauthorized',
	404: 'Not found',
	405: 'Method not allowed',
	429: 'Too many requests',
}

const rateLimits = {
	GET: 5000,
	POST: 500,
}

WebApp.connectHandlers.use('/api-data/', (req, res, next) => {
	var body = "";
	const now = new Date;  // for logging how long this function takes
	req.on('data', Meteor.bindEnvironment(data => body += data));

	req.on('end', Meteor.bindEnvironment(() => {
		res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
		const method = req.method;

		// respond to a CORS preflight request: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
		if (method=="OPTIONS" && req.headers["access-control-request-method"] && req.headers.origin) {
			res.setHeader('Access-Control-Allow-Origin', '*');
			res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
			res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
			res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours
			res.writeHead(200);
			res.end();
			return;
		}

		function sendError(code, msg) {
			console.warn('api', code, msg || codes[code], method, req.url, req.headers, body);
			res.writeHead(code, { 'Content-Type': 'text/plain'});
			res.end(msg || codes[code] || '');
		}

		// check method
		if (!rateLimits[method]) return sendError(405);

		// check auth
		const auth = (req.headers.authorization || '').match(/^basic (\S+):(\S+)$/i);
		if (!auth) return sendError(401);
		const [, userId, key] = auth;
		let user = Meteor.users.findOne({_id: userId, "apiKey.key": key}, {fields: {
			profile: 1,
			status: 1,
			'apiKey.touched': 1,
		}});
		if (!user) return sendError(401);

		// check rate limit
		if (now - user.apiKey.touched < rateLimits[method]) return sendError(429);

		// get api end point and version
		const parts = req.url.substr(1).split('/');
		let reqVersion = version; // default to latest version
		if (/^v\d+$/.test(parts[0])) {
			reqVersion = parts[0].substr(1) * 1;
			parts.shift();
		}
		req.url = parts.join('/');

		// this is my only api end point so far...
		if (req.url != 'user/status') return sendError(404);

		function finish(updated) {
			Meteor.users.update(userId, {$set: {"apiKey.touched": now}});
			if (updated) {
				// the user has been updated, fetch a new copy...
				user = Meteor.users.findOne(userId, {fields: {profile: 1, status: 1}});
			}

			const result = {
				status: 'ok',
				name: user.profile.name,
				out: user.status.out,
				location: user.status.status || '',
				time: user.status.time || '',
				history: user.status.history || [],
			};
			res.writeHead(200, { 'Content-Type': 'application/json' });
			res.end(JSON.stringify(result));
			console.log('api', method, 'v'+reqVersion, req.url, userId, body, (new Date - now)+'ms');
		}

		if (method=="GET") {
			finish();
		} else {
			let json = '';
			try {
				json = JSON.parse(body);
			} catch(e) {
				return sendError(400, 'Could not parse JSON');
			}
			// process POST request - this is async, hence defining the `finish()` callback function above...
			doSomethingAsync(json, () => finish(true /* modified  */));
		}
	}));
});

Edit: added code to respond to CORS preflight request

4 Likes

It looks like the 2020 answer for this question is ‘Use Apollo’. This page demonstrates an authenticated request: https://www.apollographql.com/docs/react/v2.5/recipes/meteor/

Someone please jump in if there’s something else I’m missing. restivus/simple:rest are virtually dead at this point (4-5 years w/ no changes, dozens of open issues, open PR’s, etc.).